Unverified Commit 489e2c46 authored by João Morado's avatar João Morado Committed by GitHub
Browse files

Cleanup TinkerReader, ForceField, and amoebaforces (#5080)

* Fresh branch refactoring the new AMOEBA code

* Finish cleaning up AmoebaAngleForce and AmoebaInPlaneAngleForce

* Cleanup AmoebaTorsionTorsionForce

* Cleanup AmoebaOutOfPlaneBend

* Cleanup AmoebaMultipoleForce

* Remove unnecessary gkForce

* Simplify usage of atomClasses in ForceField

* Formatting

* Fix type of class on WCA

* Simplify angle forces

* Add parsing of units to PiTorsion and StretchBond, and misc. formatting adjustments

* Update code per review feedback

* Clearly defined API for TorsionTorsion,  and correct matching for UB,

* Unindent break statements

* Raise ValueError if classes and types are mixed in a Urey-Bradley term definition
parent 064cee2f
...@@ -637,6 +637,7 @@ class ForceField(object): ...@@ -637,6 +637,7 @@ class ForceField(object):
self.isAngleConstrained = [] self.isAngleConstrained = []
self.constraints = {} self.constraints = {}
self.bondedToAtom = [set() for _ in self.atoms] self.bondedToAtom = [set() for _ in self.atoms]
self.atomClasses = []
# Record which atoms are bonded to each other atom # Record which atoms are bonded to each other atom
...@@ -669,6 +670,18 @@ class ForceField(object): ...@@ -669,6 +670,18 @@ class ForceField(object):
if match == site.index: if match == site.index:
self.virtualSites[atom] = (site, [matchAtoms[i].index for i in site.atoms], matchAtoms[site.excludeWith].index) self.virtualSites[atom] = (site, [matchAtoms[i].index for i in site.atoms], matchAtoms[site.excludeWith].index)
def setAtomClasses(self, forcefield):
"""
Set atom classes for all atoms in the system.
Parameters
----------
forcefield : ForceField
The ForceField object containing atom type definitions
"""
classNameForType = dict((t.name, str(t.atomClass)) for t in forcefield._atomTypes.values())
self.atomClasses = [classNameForType[self.atomType[atom]] for atom in self.atoms]
class _TemplateData(object): class _TemplateData(object):
"""Inner class used to encapsulate data about a residue template definition.""" """Inner class used to encapsulate data about a residue template definition."""
def __init__(self, name): def __init__(self, name):
...@@ -1260,6 +1273,11 @@ class ForceField(object): ...@@ -1260,6 +1273,11 @@ class ForceField(object):
elif rigidWater: elif rigidWater:
rigidResidue[res.index] = True rigidResidue[res.index] = True
# Calculate atom classes once for reuse by all generators
# Important for Amoeba forces
data.setAtomClasses(self)
# Create the System and add atoms # Create the System and add atoms
sys = mm.System() sys = mm.System()
...@@ -3531,7 +3549,6 @@ def getAtomPrint(data, atomIndex): ...@@ -3531,7 +3549,6 @@ def getAtomPrint(data, atomIndex):
return returnString return returnString
#=============================================================================================
def countConstraint(data): def countConstraint(data):
...@@ -3550,64 +3567,35 @@ def countConstraint(data): ...@@ -3550,64 +3567,35 @@ def countConstraint(data):
## @private ## @private
class AmoebaBondGenerator(object): class AmoebaBondGenerator(object):
#=============================================================================================
"""An AmoebaBondGenerator constructs a AmoebaBondForce.""" """An AmoebaBondGenerator constructs a AmoebaBondForce."""
#=============================================================================================
def __init__(self, cubic, quartic): def __init__(self, cubic, quartic):
self.builder = amoebaforces.AmoebaBondForceBuilder(cubic, quartic)
self.cubic = cubic
self.quartic = quartic
self.types1 = []
self.types2 = []
self.length = []
self.k = []
#=============================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <AmoebaBondForce bond-cubic="-25.5" bond-quartic="379.3125"> # <AmoebaBondForce bond-cubic="-25.5" bond-quartic="379.3125">
# <Bond class1="1" class2="2" length="0.1437" k="156900.0"/> # <Bond class1="1" class2="2" length="0.1437" k="156900.0"/>
generator = AmoebaBondGenerator(element.attrib['bond-cubic'], element.attrib['bond-quartic']) generator = AmoebaBondGenerator(element.attrib['bond-cubic'], element.attrib['bond-quartic'])
forceField._forces.append(generator) forceField._forces.append(generator)
for bond in element.findall('Bond'): for bond in element.findall('Bond'):
types = forceField._findAtomTypes(bond.attrib, 2) try:
if None not in types: generator.builder.registerParams((bond.attrib['class1'], bond.attrib['class2']), float(bond.attrib['length']), float(bond.attrib['k']))
generator.types1.append(types[0]) except:
generator.types2.append(types[1]) outputString = "AmoebaBondGenerator: error getting classes: %s %s" % (bond.attrib['class1'], bond.attrib['class2'])
generator.length.append(float(bond.attrib['length']))
generator.k.append(float(bond.attrib['k']))
else:
outputString = "AmoebaBondGenerator: error getting types: %s %s" % (
bond.attrib['class1'],
bond.attrib['class2'])
raise ValueError(outputString) raise ValueError(outputString)
#=============================================================================================
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
builder = amoebaforces.AmoebaBondForceBuilder(self.cubic, self.quartic) force = self.builder.getForce(sys)
force = builder.getForce(sys) bondsConstraints = []
for bond in data.bonds: for bond in data.bonds:
type1 = data.atomType[data.atoms[bond.atom1]] bondsConstraints.append(bond.isConstrained)
type2 = data.atomType[data.atoms[bond.atom2]] bondType = (data.atomClasses[bond.atom1], data.atomClasses[bond.atom2])
for i in range(len(self.types1)): params = self.builder._findMatchingParams(self.builder.bondParams, bondType)
types1 = self.types1[i] bond.length = params[0]
types2 = self.types2[i]
if (type1 in types1 and type2 in types2) or (type1 in types2 and type2 in types1):
bond.length = self.length[i]
if bond.isConstrained: if bond.isConstrained:
data.addConstraint(sys, bond.atom1, bond.atom2, self.length[i]) data.addConstraint(sys, bond.atom1, bond.atom2, params[0])
if self.k[i] != 0: self.builder.addBonds(force, data.atomClasses, _findBondsForExclusions(data, sys), bondsConstraints, args.get('flexibleConstraints', False))
if not bond.isConstrained or args.get('flexibleConstraints', False):
force.addBond(bond.atom1, bond.atom2, [self.length[i], self.k[i]])
break
parsers["AmoebaBondForce"] = AmoebaBondGenerator.parseElement parsers["AmoebaBondForce"] = AmoebaBondGenerator.parseElement
...@@ -3639,38 +3627,20 @@ def addAngleConstraint(angle, idealAngle, data, sys): ...@@ -3639,38 +3627,20 @@ def addAngleConstraint(angle, idealAngle, data, sys):
data.addConstraint(sys, angle[0], angle[2], length) data.addConstraint(sys, angle[0], angle[2], length)
return return
#=============================================================================================
## @private ## @private
class AmoebaAngleGenerator(object): class AmoebaAngleGenerator(object):
#=============================================================================================
"""An AmoebaAngleGenerator constructs a AmoebaAngleForce.""" """An AmoebaAngleGenerator constructs a AmoebaAngleForce."""
#=============================================================================================
def __init__(self, forceField, cubic, quartic, pentic, sextic): def __init__(self, forceField, cubic, quartic, pentic, sextic):
self.angleBuilder = amoebaforces.AmoebaAngleForceBuilder(float(cubic), float(quartic), float(pentic), float(sextic))
self.inPlaneAngleBuilder = amoebaforces.AmoebaInPlaneAngleForceBuilder(float(cubic), float(quartic), float(pentic), float(sextic))
self.forceField = forceField self.forceField = forceField
self.cubic = cubic self.angleParams = {}
self.quartic = quartic
self.pentic = pentic
self.sextic = sextic
self.types1 = []
self.types2 = []
self.types3 = []
self.angle = []
self.k = []
self.inPlane = []
#=============================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <AmoebaAngleForce angle-cubic="-0.014" angle-quartic="5.6e-05" angle-pentic="-7e-07" angle-sextic="2.2e-08"> # <AmoebaAngleForce angle-cubic="-0.014" angle-quartic="5.6e-05" angle-pentic="-7e-07" angle-sextic="2.2e-08">
# <Angle class1="2" class2="1" class3="3" k="0.0637259642196" angle1="122.00" /> # <Angle class1="2" class2="1" class3="3" k="0.0637259642196" angle1="122.00" />
existing = [f for f in forceField._forces if isinstance(f, AmoebaAngleGenerator)] existing = [f for f in forceField._forces if isinstance(f, AmoebaAngleGenerator)]
if len(existing) == 0: if len(existing) == 0:
generator = AmoebaAngleGenerator(forceField, element.attrib['angle-cubic'], element.attrib['angle-quartic'], element.attrib['angle-pentic'], element.attrib['angle-sextic']) generator = AmoebaAngleGenerator(forceField, element.attrib['angle-cubic'], element.attrib['angle-quartic'], element.attrib['angle-pentic'], element.attrib['angle-sextic'])
...@@ -3680,29 +3650,19 @@ class AmoebaAngleGenerator(object): ...@@ -3680,29 +3650,19 @@ class AmoebaAngleGenerator(object):
if tuple(element.attrib[x] for x in ('angle-cubic', 'angle-quartic', 'angle-pentic', 'angle-sextic')) != (generator.cubic, generator.quartic, generator.pentic, generator.sextic): if tuple(element.attrib[x] for x in ('angle-cubic', 'angle-quartic', 'angle-pentic', 'angle-sextic')) != (generator.cubic, generator.quartic, generator.pentic, generator.sextic):
raise ValueError('All <AmoebaAngleForce> tags must use identical scale factors') raise ValueError('All <AmoebaAngleForce> tags must use identical scale factors')
for angle in element.findall('Angle'): for angle in element.findall('Angle'):
types = forceField._findAtomTypes(angle.attrib, 3) try:
if None not in types: theta0 = [float(angle.attrib['angle1'])]
generator.types1.append(types[0])
generator.types2.append(types[1])
generator.types3.append(types[2])
angleList = []
angleList.append(float(angle.attrib['angle1']))
if 'angle2' in angle.attrib: if 'angle2' in angle.attrib:
angleList.append(float(angle.attrib['angle2'])) theta0.append(float(angle.attrib['angle2']))
if 'angle3' in angle.attrib: if 'angle3' in angle.attrib:
angleList.append(float(angle.attrib['angle3'])) theta0.append(float(angle.attrib['angle3']))
generator.angleParams[(angle.attrib['class1'],
generator.angle.append(angleList) angle.attrib['class2'],
generator.k.append(float(angle.attrib['k'])) angle.attrib['class3'])] = {"k": float(angle.attrib['k']),
if 'inPlane' in angle.attrib: "theta0": theta0,
generator.inPlane.append(angle.attrib['inPlane'].lower() == 'true') "inPlane": angle.attrib.get('inPlane', None)}
else: except Exception as e:
generator.inPlane.append(None) # for backward compatibility with older versions of AMOEBA outputString = "AmoebaAngleGenerator: error getting classes: %s %s %s" % (
else:
outputString = "AmoebaAngleGenerator: error getting types: %s %s %s" % (
angle.attrib['class1'], angle.attrib['class1'],
angle.attrib['class2'], angle.attrib['class2'],
angle.attrib['class3']) angle.attrib['class3'])
...@@ -3722,87 +3682,61 @@ class AmoebaAngleGenerator(object): ...@@ -3722,87 +3682,61 @@ class AmoebaAngleGenerator(object):
# non-in-plane angles # non-in-plane angles
#============================================================================================= #=============================================================================================
def createForcePostOpBendAngle(self, sys, data, nonbondedMethod, nonbondedCutoff, angleList, args): def createForcePostOpBendAngle(self, sys, data, nonbondedMethod, nonbondedCutoff, angles, anglesConstraints, args):
builder = amoebaforces.AmoebaAngleForceBuilder(self.cubic, self.quartic, self.pentic, self.sextic) idealAngles = {}
force = builder.getForce(sys) for angle, isConstrained in zip(angles, anglesConstraints):
DEG_TO_RAD = math.pi / 180 angleClasses = (data.atomClasses[angle[0]], data.atomClasses[angle[1]], data.atomClasses[angle[2]])
for angleDict in angleList: params = self.angleParams.get(angleClasses) or self.angleParams.get(angleClasses[::-1])
angle = angleDict['angle'] if params is None:
isConstrained = angleDict['isConstrained'] raise ValueError(
inPlane = angleDict['inPlane'] f"AmoebaAngleGenerator: no parameters found for angle: "
f"{angleClasses[0]}-{angleClasses[2]}-"
type1 = data.atomType[data.atoms[angle[0]]] f"{getAtomPrint(data, angle[0])}, "
type2 = data.atomType[data.atoms[angle[1]]] f"{getAtomPrint(data, angle[1])}, "
type3 = data.atomType[data.atoms[angle[2]]] f"{getAtomPrint(data, angle[2])}"
for i in range(len(self.types1)): )
# self.inPlane is used for modern force fields. inPlane is used for legacy ones that don't specify it. angleTuple = tuple(angle)
if self.inPlane[i] or (self.inPlane[i] is None and inPlane): if isConstrained and params["k"] != 0.0:
continue idealAngle = params["theta0"][0]
types1 = self.types1[i] addAngleConstraint(angle, idealAngle*math.pi/180.0, data, sys)
types2 = self.types2[i]
types3 = self.types3[i]
if (type1 in types1 and type2 in types2 and type3 in types3) or (type1 in types3 and type2 in types2 and type3 in types1):
if isConstrained and self.k[i] != 0.0:
angleDict['idealAngle'] = self.angle[i][0]
addAngleConstraint(angle, self.angle[i][0]*DEG_TO_RAD, data, sys)
if self.k[i] != 0 and (not isConstrained or args.get('flexibleConstraints', False)):
lenAngle = len(self.angle[i])
if (lenAngle > 1):
# get k-index by counting number of non-angle hydrogens on the central atom
# based on kangle.f
numberOfHydrogens = 0
for bond in data.atomBonds[angle[1]]:
atom1 = data.bonds[bond].atom1
atom2 = data.bonds[bond].atom2
if (atom1 == angle[1] and atom2 != angle[0] and atom2 != angle[2] and (sys.getParticleMass(atom2)/unit.dalton) < 1.90):
numberOfHydrogens += 1
if (atom2 == angle[1] and atom1 != angle[0] and atom1 != angle[2] and (sys.getParticleMass(atom1)/unit.dalton) < 1.90):
numberOfHydrogens += 1
if (numberOfHydrogens < lenAngle):
angleValue = self.angle[i][numberOfHydrogens]
else:
outputString = "AmoebaAngleGenerator angle index=%d is out of range: [0, %5d] " % (numberOfHydrogens, lenAngle)
raise ValueError(outputString)
else: else:
angleValue = self.angle[i][0] idealAngle = self.angleBuilder.getIdealAngle(angleTuple, params["theta0"], data.atoms, data.bondedToAtom)
self.angleBuilder.registerParams(angleTuple, (idealAngle, params["k"]))
angleDict['idealAngle'] = angleValue idealAngles[angleTuple] = idealAngle*math.pi/180.0
force.addAngle(angle[0], angle[1], angle[2], [angleValue, self.k[i]]) force = self.angleBuilder.getForce(sys)
break self.angleBuilder.addAngles(force, angles, anglesConstraints, args.get('flexibleConstraints', False))
return idealAngles
#============================================================================================= #=============================================================================================
# createForcePostOpBendInPlaneAngle is called by AmoebaOutOfPlaneBendForce with the list of # createForcePostOpBendInPlaneAngle is called by AmoebaOutOfPlaneBendForce with the list of
# in-plane angles # in-plane angles
#============================================================================================= #=============================================================================================
def createForcePostOpBendInPlaneAngle(self, sys, data, nonbondedMethod, nonbondedCutoff, angleList, args): def createForcePostOpBendInPlaneAngle(self, sys, data, nonbondedMethod, nonbondedCutoff, inPlaneAngles, inPlaneAnglesConstraints, args):
builder = amoebaforces.AmoebaInPlaneAngleForceBuilder(self.cubic, self.quartic, self.pentic, self.sextic) idealAngles = {}
force = builder.getForce(sys) for inPlaneAngle, isConstrained in zip(inPlaneAngles, inPlaneAnglesConstraints):
for angleDict in angleList: angleClasses = (data.atomClasses[inPlaneAngle[0]], data.atomClasses[inPlaneAngle[1]], data.atomClasses[inPlaneAngle[2]])
params = self.angleParams.get(angleClasses) or self.angleParams.get(angleClasses[::-1])
angle = angleDict['angle'] if params is None:
isConstrained = angleDict['isConstrained'] raise ValueError(
inPlane = angleDict['inPlane'] f"AmoebaAngleGenerator: no parameters found for angle: "
f"{angleClasses[0]}-{angleClasses[2]}-"
type1 = data.atomType[data.atoms[angle[0]]] f"{getAtomPrint(data, inPlaneAngle[0])}, "
type2 = data.atomType[data.atoms[angle[1]]] f"{getAtomPrint(data, inPlaneAngle[1])}, "
type3 = data.atomType[data.atoms[angle[2]]] f"{getAtomPrint(data, inPlaneAngle[2])}"
)
for i in range(len(self.types1)): angleTuple = tuple(inPlaneAngle)
# self.inPlane is used for modern force fields. inPlane is used for legacy ones that don't specify it. if isConstrained and params["k"] != 0.0:
if self.inPlane[i] == False or (self.inPlane[i] is None and not inPlane): idealAngle = params["theta0"][0]
continue addAngleConstraint(inPlaneAngle, idealAngle*math.pi/180.0, data, sys)
types1 = self.types1[i] else:
types2 = self.types2[i] idealAngle = self.angleBuilder.getIdealAngle(angleTuple, params["theta0"], data.atoms, data.bondedToAtom)
types3 = self.types3[i] self.inPlaneAngleBuilder.registerParams(angleClasses, (idealAngle, params["k"]))
idealAngles[angleTuple[:3]] = idealAngle*math.pi/180.0
force = self.inPlaneAngleBuilder.getForce(sys)
self.inPlaneAngleBuilder.addInPlaneAngles(force, data.atomClasses, inPlaneAngles, inPlaneAnglesConstraints, args.get('flexibleConstraints', False))
if (type1 in types1 and type2 in types2 and type3 in types3) or (type1 in types3 and type2 in types2 and type3 in types1): return idealAngles
angleDict['idealAngle'] = self.angle[i][0]
if (isConstrained and self.k[i] != 0.0):
addAngleConstraint(angle, self.angle[i][0]*math.pi/180.0, data, sys)
if self.k[i] != 0.0 and (not isConstrained or args.get('flexibleConstraints', False)):
force.addBond((angle[0], angle[1], angle[2], angle[3]), (self.angle[i][0], self.k[i]))
break
parsers["AmoebaAngleForce"] = AmoebaAngleGenerator.parseElement parsers["AmoebaAngleForce"] = AmoebaAngleGenerator.parseElement
...@@ -3813,56 +3747,14 @@ parsers["AmoebaAngleForce"] = AmoebaAngleGenerator.parseElement ...@@ -3813,56 +3747,14 @@ parsers["AmoebaAngleForce"] = AmoebaAngleGenerator.parseElement
#============================================================================================= #=============================================================================================
## @private ## @private
class AmoebaOutOfPlaneBendGenerator(object): class AmoebaOutOfPlaneBendGenerator(object):
#=============================================================================================
"""An AmoebaOutOfPlaneBendGenerator constructs a AmoebaOutOfPlaneBendForce.""" """An AmoebaOutOfPlaneBendGenerator constructs a AmoebaOutOfPlaneBendForce."""
#=============================================================================================
def __init__(self, forceField, type, cubic, quartic, pentic, sextic): def __init__(self, forceField, type, cubic, quartic, pentic, sextic):
self.forceField = forceField self.forceField = forceField
self.type = type self.builder = amoebaforces.AmoebaOutOfPlaneBendForceBuilder(float(cubic), float(quartic), float(pentic), float(sextic))
self.cubic = cubic
self.quartic = quartic
self.pentic = pentic
self.sextic = sextic
self.types1 = []
self.types2 = []
self.types3 = []
self.types4 = []
self.ks = []
#=============================================================================================
# Local version of findAtomTypes needed since class indices are 0 (i.e., not recognized)
# for types3 and 4
#=============================================================================================
def findAtomTypes(self, forceField, node, num):
"""Parse the attributes on an XML tag to find the set of atom types for each atom it involves."""
types = []
attrib = node.attrib
for i in range(num):
if num == 1:
suffix = ''
else:
suffix = str(i+1)
classAttrib = 'class'+suffix
if classAttrib in attrib:
if attrib[classAttrib] in forceField._atomClasses:
types.append(forceField._atomClasses[attrib[classAttrib]])
else:
types.append(set())
return types
#=============================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <AmoebaOutOfPlaneBendForce type="ALLINGER" opbend-cubic="-0.014" opbend-quartic="5.6e-05" opbend-pentic="-7e-07" opbend-sextic="2.2e-08"> # <AmoebaOutOfPlaneBendForce type="ALLINGER" opbend-cubic="-0.014" opbend-quartic="5.6e-05" opbend-pentic="-7e-07" opbend-sextic="2.2e-08">
# <Angle class1="2" class2="1" class3="0" class4="0" k="0.0531474541591"/> # <Angle class1="2" class2="1" class3="0" class4="0" k="0.0531474541591"/>
# <Angle class1="3" class2="1" class3="0" class4="0" k="0.0898536095496"/> # <Angle class1="3" class2="1" class3="0" class4="0" k="0.0898536095496"/>
...@@ -3879,154 +3771,62 @@ class AmoebaOutOfPlaneBendGenerator(object): ...@@ -3879,154 +3771,62 @@ class AmoebaOutOfPlaneBendGenerator(object):
raise ValueError('All <AmoebaOutOfPlaneBendForce> tags must use identical scale factors') raise ValueError('All <AmoebaOutOfPlaneBendForce> tags must use identical scale factors')
for angle in element.findall('Angle'): for angle in element.findall('Angle'):
if 'class3' in angle.attrib and 'class4' in angle.attrib and angle.attrib['class3'] == '0' and angle.attrib['class4'] == '0': try:
# This is needed for backward compatibility with old AMOEBA force fields that specified wildcards in a nonstandard way. generator.builder.registerParams((angle.attrib['class1'], angle.attrib['class2'], angle.attrib['class3'], angle.attrib['class4']), (float(angle.attrib['k']),))
angle.attrib['class3'] = '' except:
angle.attrib['class4'] = '' outputString = "AmoebaOutOfPlaneBendGenerator error getting classes: %s %s %s %s." % (
types = generator.findAtomTypes(forceField, angle, 4)
if types is not None:
generator.types1.append(types[0])
generator.types2.append(types[1])
generator.types3.append(types[2])
generator.types4.append(types[3])
generator.ks.append(float(angle.attrib['k']))
else:
outputString = "AmoebaOutOfPlaneBendGenerator error getting types: %s %s %s %s." % (
angle.attrib['class1'], angle.attrib['class2'], angle.attrib['class3'], angle.attrib['class4']) angle.attrib['class1'], angle.attrib['class2'], angle.attrib['class3'], angle.attrib['class4'])
raise ValueError(outputString) raise ValueError(outputString)
#=============================================================================================
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
self._nonbondedMethod = nonbondedMethod self._nonbondedMethod = nonbondedMethod
self._nonbondedCutoff = nonbondedCutoff self._nonbondedCutoff = nonbondedCutoff
def postprocessSystem(self, sys, data, args): def postprocessSystem(self, sys, data, args):
# We need to wait until after all bonds have been added so their lengths will be set correctly. # We need to wait until after all bonds have been added so their lengths will be set correctly.
# Classify angles into in-plane, out-of-plane, and generic angles
opbendTypes = [opbendType for opbendType, _ in self.builder.outOfPlaneBendParams]
inPlaneAngles, outOfPlaneAngles, genericAngles = self.builder.classifyAngles(
data.angles,
data.atomClasses,
data.bondedToAtom,
opbendTypes
)
builder = amoebaforces.AmoebaOutOfPlaneBendForceBuilder(self.cubic, self.quartic, self.pentic, self.sextic) # Add out-of-plane bend force
force = builder.getForce(sys) force = self.builder.getForce(sys)
self.builder.addOutOfPlaneBends(force, data.atomClasses, outOfPlaneAngles)
# this hash is used to ensure the out-of-plane-bend bonds
# are only added once # Constraints lists
inPlaneAnglesConstraints = []
skipAtoms = dict() genericAnglesConstraints = []
angles = [] inPlaneAnglesCmp = [tuple(angle)[:3] for angle in inPlaneAngles]
for angle, isConstrained in zip(data.angles, data.isAngleConstrained):
def addBond(particles): if angle in genericAngles:
types = [data.atomType[data.atoms[p]] for p in particles] genericAnglesConstraints.append(isConstrained)
for i in range(len(self.types1)): elif angle in inPlaneAnglesCmp:
if types[1] in self.types2[i] and types[3] in self.types1[i]: inPlaneAnglesConstraints.append(isConstrained)
if (types[0] in self.types3[i] and types[2] in self.types4[i]) or (types[2] in self.types3[i] and types[0] in self.types4[i]):
force.addBond(particles, [self.ks[i]]) # Get AmoebaAngleGenerator and add AmoebaAngle and AmoebaInPlaneAngle forces
return idealAngles = {}
for (angle, isConstrained) in zip(data.angles, data.isAngleConstrained):
middleAtom = angle[1]
middleType = data.atomType[data.atoms[middleAtom]]
middleCovalency = len(data.atomBonds[middleAtom])
# if middle atom has covalency of 3 and
# the types of the middle atom and the partner atom (atom bonded to
# middle atom, but not in angle) match types1 and types2, then
# three out-of-plane bend angles are generated. Three in-plane angle
# are also generated. If the conditions are not satisfied, the angle is marked as 'generic' angle (not a in-plane angle)
if middleCovalency == 3 and middleAtom not in skipAtoms:
partners = []
for bond in data.atomBonds[middleAtom]:
atom1 = data.bonds[bond].atom1
atom2 = data.bonds[bond].atom2
if atom1 != middleAtom:
partner = atom1
else:
partner = atom2
partnerType = data.atomType[data.atoms[partner]]
for i in range(len(self.types1)):
types1 = self.types1[i]
types2 = self.types2[i]
if middleType in types2 and partnerType in types1:
partners.append(partner)
break
if len(partners) == 3:
addBond([partners[0], middleAtom, partners[1], partners[2]])
addBond([partners[2], middleAtom, partners[0], partners[1]])
addBond([partners[1], middleAtom, partners[2], partners[0]])
# skipAtoms is used to ensure angles are only included once
skipAtoms[middleAtom] = set(partners[:3])
# in-plane angle
angleDict = {}
angleList = list(angle[:3])
for atomIndex in partners:
if atomIndex not in angleList:
angleList.append(atomIndex)
angleDict['angle'] = angleList
angleDict['isConstrained'] = 0
angleDict['inPlane'] = True
angles.append(angleDict)
else:
angleDict = {}
angleList = list(angle[:3])
for atomIndex in partners:
if atomIndex not in angleList:
angleList.append(atomIndex)
angleDict['angle'] = angleList
angleDict['isConstrained'] = isConstrained
angleDict['inPlane'] = False
angles.append(angleDict)
elif middleCovalency == 3 and middleAtom in skipAtoms:
angleDict = {}
angleList = list(angle[:3])
for atomIndex in skipAtoms[middleAtom]:
if atomIndex not in angleList:
angleList.append(atomIndex)
angleDict['angle'] = angleList
angleDict['isConstrained'] = isConstrained
angleDict['inPlane'] = True
angles.append(angleDict)
else:
angleDict = {}
angleDict['angle'] = angle
angleDict['isConstrained'] = isConstrained
angleDict['inPlane'] = False
angles.append(angleDict)
# get AmoebaAngleGenerator and add AmoebaAngle and AmoebaInPlaneAngle forces
for force in self.forceField._forces: for force in self.forceField._forces:
if (force.__class__.__name__ == 'AmoebaAngleGenerator'): if (force.__class__.__name__ == 'AmoebaAngleGenerator'):
force.createForcePostOpBendAngle(sys, data, self._nonbondedMethod, self._nonbondedCutoff, angles, args) idealAnglesTmp = force.createForcePostOpBendAngle(sys, data, self._nonbondedMethod, self._nonbondedCutoff, genericAngles, genericAnglesConstraints, args)
force.createForcePostOpBendInPlaneAngle(sys, data, self._nonbondedMethod, self._nonbondedCutoff, angles, args) idealAngles.update(idealAnglesTmp)
idealAnglesTmp = force.createForcePostOpBendInPlaneAngle(sys, data, self._nonbondedMethod, self._nonbondedCutoff, inPlaneAngles, inPlaneAnglesConstraints, args)
idealAngles.update(idealAnglesTmp)
for force in self.forceField._forces: for force in self.forceField._forces:
if (force.__class__.__name__ == 'AmoebaStretchBendGenerator'): if (force.__class__.__name__ == 'AmoebaStretchBendGenerator'):
force.createForcePostAmoebaBondForce(sys, data, self._nonbondedMethod, self._nonbondedCutoff, angles, args) force.createForcePostAmoebaBondForce(sys, data, self._nonbondedMethod, self._nonbondedCutoff, data.angles, idealAngles, args)
parsers["AmoebaOutOfPlaneBendForce"] = AmoebaOutOfPlaneBendGenerator.parseElement parsers["AmoebaOutOfPlaneBendForce"] = AmoebaOutOfPlaneBendGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaTorsionGenerator(object): class AmoebaTorsionGenerator(object):
# TODO: remove once amoeba2013.xml has been updated
#=============================================================================================
"""An AmoebaTorsionGenerator constructs a AmoebaTorsionForce.""" """An AmoebaTorsionGenerator constructs a AmoebaTorsionForce."""
#=============================================================================================
def __init__(self, torsionUnit): def __init__(self, torsionUnit):
self.torsionUnit = torsionUnit self.torsionUnit = torsionUnit
self.types1 = [] self.types1 = []
...@@ -4038,11 +3838,8 @@ class AmoebaTorsionGenerator(object): ...@@ -4038,11 +3838,8 @@ class AmoebaTorsionGenerator(object):
self.t2 = [] self.t2 = []
self.t3 = [] self.t3 = []
#=============================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <AmoebaTorsionForce torsionUnit="0.5"> # <AmoebaTorsionForce torsionUnit="0.5">
# <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" /> # <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" />
# <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" /> # <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" />
...@@ -4079,15 +3876,13 @@ class AmoebaTorsionGenerator(object): ...@@ -4079,15 +3876,13 @@ class AmoebaTorsionGenerator(object):
generator.t3.append(tInfo) generator.t3.append(tInfo)
else: else:
outputString = "AmoebaTorsionGenerator: error getting types: %s %s %s %s" % ( outputString = "AmoebaTorsionGenerator: error getting classes: %s %s %s %s" % (
torsion.attrib['class1'], torsion.attrib['class1'],
torsion.attrib['class2'], torsion.attrib['class2'],
torsion.attrib['class3'], torsion.attrib['class3'],
torsion.attrib['class4']) torsion.attrib['class4'])
raise ValueError(outputString) raise ValueError(outputString)
#=============================================================================================
def createForce(self, sys, data, nontorsionedMethod, nontorsionedCutoff, args): def createForce(self, sys, data, nontorsionedMethod, nontorsionedCutoff, args):
builder = amoebaforces.AmoebaTorsionForceBuilder() builder = amoebaforces.AmoebaTorsionForceBuilder()
force = builder.getForce(sys) force = builder.getForce(sys)
...@@ -4119,126 +3914,61 @@ class AmoebaTorsionGenerator(object): ...@@ -4119,126 +3914,61 @@ class AmoebaTorsionGenerator(object):
parsers["AmoebaTorsionForce"] = AmoebaTorsionGenerator.parseElement parsers["AmoebaTorsionForce"] = AmoebaTorsionGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaPiTorsionGenerator(object): class AmoebaPiTorsionGenerator(object):
#=============================================================================================
"""An AmoebaPiTorsionGenerator constructs a AmoebaPiTorsionForce.""" """An AmoebaPiTorsionGenerator constructs a AmoebaPiTorsionForce."""
#=============================================================================================
def __init__(self): def __init__(self):
self.types1 = [] self.builder = amoebaforces.AmoebaPiTorsionForceBuilder()
self.types2 = []
self.k = []
#=============================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <AmoebaPiTorsionForce piTorsionUnit="1.0">
# <PiTorsion class1="1" class2="3" k="28.6604" />
# <PiTorsion class1="3" class2="15" k="28.6604" />
generator = AmoebaPiTorsionGenerator() generator = AmoebaPiTorsionGenerator()
forceField._forces.append(generator) forceField._forces.append(generator)
piTorsionUnit = float(element.attrib['piTorsionUnit']) if 'piTorsionUnit' in element.attrib else 1.0
for piTorsion in element.findall('PiTorsion'): for piTorsion in element.findall('PiTorsion'):
types = forceField._findAtomTypes(piTorsion.attrib, 2) try:
if None not in types: k=float(piTorsion.attrib['k'])*piTorsionUnit
generator.types1.append(types[0]) generator.builder.registerParams((piTorsion.attrib['class1'], piTorsion.attrib['class2']), (k,))
generator.types2.append(types[1]) except:
generator.k.append(float(piTorsion.attrib['k'])) outputString = "AmoebaPiTorsionGenerator: error getting classes: %s %s " % (
else:
outputString = "AmoebaPiTorsionGenerator: error getting types: %s %s " % (
piTorsion.attrib['class1'], piTorsion.attrib['class1'],
piTorsion.attrib['class2']) piTorsion.attrib['class2'])
raise ValueError(outputString) raise ValueError(outputString)
#=============================================================================================
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
builder = amoebaforces.AmoebaPiTorsionForceBuilder() processedPiTorsions = self.builder.getAllPiTorsions(data.atomClasses, data.bondedToAtom, _findBondsForExclusions(data, sys))
force = builder.getForce(sys) force = self.builder.getForce(sys)
self.builder.addPiTorsions(force, data.atomClasses, processedPiTorsions)
for bond1 in data.bonds:
# search for bonds with both atoms in bond having covalency == 3
atom1 = bond1.atom1
atom2 = bond1.atom2
if (len(data.atomBonds[atom1]) == 3 and len(data.atomBonds[atom2]) == 3):
type1 = data.atomType[data.atoms[atom1]]
type2 = data.atomType[data.atoms[atom2]]
for i in range(len(self.types1)):
types1 = self.types1[i]
types2 = self.types2[i]
if (type1 in types1 and type2 in types2) or (type1 in types2 and type2 in types1):
# piTorsionAtom1, piTorsionAtom2 are the atoms bonded to atom1, excluding atom2
# piTorsionAtom5, piTorsionAtom6 are the atoms bonded to atom2, excluding atom1
piTorsionAtom1 = -1
piTorsionAtom2 = -1
piTorsionAtom3 = atom1
piTorsionAtom4 = atom2
piTorsionAtom5 = -1
piTorsionAtom6 = -1
for bond in data.atomBonds[atom1]:
bondedAtom1 = data.bonds[bond].atom1
bondedAtom2 = data.bonds[bond].atom2
if (bondedAtom1 != atom1):
b1 = bondedAtom1
else:
b1 = bondedAtom2
if (b1 != atom2):
if (piTorsionAtom1 == -1):
piTorsionAtom1 = b1
else:
piTorsionAtom2 = b1
for bond in data.atomBonds[atom2]:
bondedAtom1 = data.bonds[bond].atom1
bondedAtom2 = data.bonds[bond].atom2
if (bondedAtom1 != atom2):
b1 = bondedAtom1
else:
b1 = bondedAtom2
if (b1 != atom1):
if (piTorsionAtom5 == -1):
piTorsionAtom5 = b1
else:
piTorsionAtom6 = b1
force.addBond([piTorsionAtom1, piTorsionAtom2, piTorsionAtom3, piTorsionAtom4, piTorsionAtom5, piTorsionAtom6], [self.k[i]])
parsers["AmoebaPiTorsionForce"] = AmoebaPiTorsionGenerator.parseElement parsers["AmoebaPiTorsionForce"] = AmoebaPiTorsionGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaStretchTorsionGenerator(object): class AmoebaStretchTorsionGenerator(object):
"""An AmoebaStretchTorsionGenerator constructs a AmoebaStretchTorsionForce.""" """An AmoebaStretchTorsionGenerator constructs a AmoebaStretchTorsionForce."""
def __init__(self): def __init__(self):
self.torsions = [] self.builder = amoebaforces.AmoebaStretchTorsionForceBuilder()
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <Torsion class1="44" class2="46" class3="68" class4="65" v11="0.0" v12="0.0" v13="62.760000000000005" v21="0.0" v22="0.0" v23="-167.36" v31="0.0" v32="0.0" v33="217.568"/>
generator = AmoebaStretchTorsionGenerator() generator = AmoebaStretchTorsionGenerator()
forceField._forces.append(generator) forceField._forces.append(generator)
params = ('v11', 'v12', 'v13', 'v21', 'v22', 'v23', 'v31', 'v32', 'v33')
for torsion in element.findall('Torsion'): for torsion in element.findall('Torsion'):
types = forceField._findAtomTypes(torsion.attrib, 4) try:
if None not in types: params = tuple(float(torsion.attrib[p]) for p in ('v11', 'v12', 'v13', 'v21', 'v22', 'v23', 'v31', 'v32', 'v33'))
v = [float(torsion.attrib[param]) for param in params] generator.builder.registerParams((torsion.attrib['class1'], torsion.attrib['class2'], torsion.attrib['class3'], torsion.attrib['class4']), params)
generator.torsions.append((types, v)) except Exception as e:
generator.classNameForType = dict((t.name, int(t.atomClass)) for t in forceField._atomTypes.values()) outputString = "AmoebaStretchTorsionGenerator: error getting classes: %s %s %s %s" % (
torsion.attrib['class1'],
torsion.attrib['class2'],
torsion.attrib['class3'],
torsion.attrib['class4'])
raise ValueError(outputString)
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
pass pass
...@@ -4246,67 +3976,35 @@ class AmoebaStretchTorsionGenerator(object): ...@@ -4246,67 +3976,35 @@ class AmoebaStretchTorsionGenerator(object):
def postprocessSystem(self, sys, data, args): def postprocessSystem(self, sys, data, args):
# We need to wait until after all bonds and torsions have been added before adding the stretch-torsions, # We need to wait until after all bonds and torsions have been added before adding the stretch-torsions,
# since it needs parameters from them. # since it needs parameters from them.
force = self.builder.getForce(sys)
builder = amoebaforces.AmoebaStretchTorsionForceBuilder() self.builder.addStretchTorsions(sys, force, data.atomClasses, data.propers)
force = builder.getForce(sys)
# Record parameters for bonds and torsions so we can look them up quickly.
bondForce = [f for f in sys.getForces() if type(f) == mm.CustomBondForce and f.getName() == 'AmoebaBond'][0]
torsionForce = [f for f in sys.getForces() if type(f) == mm.PeriodicTorsionForce][0]
bondLength = {}
torsionPhase = defaultdict(lambda: [0.0, math.pi, 0.0])
for i in range(bondForce.getNumBonds()):
p1, p2, params = bondForce.getBondParameters(i)
bondLength[(p1, p2)] = params[0]
bondLength[(p2, p1)] = params[0]
for i in range(torsionForce.getNumTorsions()):
p1, p2, p3, p4, periodicity, phase, k = torsionForce.getTorsionParameters(i)
if periodicity < 4:
phase = phase.value_in_unit(unit.radian)
torsionPhase[(p1, p2, p3, p4)][periodicity-1] = phase
torsionPhase[(p4, p3, p2, p1)][periodicity-1] = phase
# Add stretch-torsions.
for torsion in data.propers:
type1 = data.atomType[data.atoms[torsion[0]]]
type2 = data.atomType[data.atoms[torsion[1]]]
type3 = data.atomType[data.atoms[torsion[2]]]
type4 = data.atomType[data.atoms[torsion[3]]]
for types, v in self.torsions:
if (type1 in types[3] and type2 in types[2] and type3 in types[1] and type4 in types[0]):
type1, type2, type3, type4 = type4, type3, type2, type1
torsion = tuple(reversed(torsion))
if (type1 in types[0] and type2 in types[1] and type3 in types[2] and type4 in types[3]):
params = list(v)
params.append(bondLength[(torsion[0], torsion[1])])
params.append(bondLength[(torsion[1], torsion[2])])
params.append(bondLength[(torsion[2], torsion[3])])
params += torsionPhase[torsion]
force.addBond(torsion, params)
break
parsers["AmoebaStretchTorsionForce"] = AmoebaStretchTorsionGenerator.parseElement parsers["AmoebaStretchTorsionForce"] = AmoebaStretchTorsionGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaAngleTorsionGenerator(object): class AmoebaAngleTorsionGenerator(object):
"""An AmoebaAngleTorsionGenerator constructs a AmoebaAngleTorsionForce.""" """An AmoebaAngleTorsionGenerator constructs a AmoebaAngleTorsionForce."""
def __init__(self): def __init__(self):
self.torsions = [] self.builder = amoebaforces.AmoebaAngleTorsionForceBuilder()
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <AmoebaAngleTorsionForce>
# <Torsion class1="44" class2="46" class3="68" class4="65" v11="3.3555680000000003" v12="0.0" v13="-13.903432" v21="0.0" v22="0.0" v23="-2.63592"/>
generator = AmoebaAngleTorsionGenerator() generator = AmoebaAngleTorsionGenerator()
forceField._forces.append(generator) forceField._forces.append(generator)
params = ('v11', 'v12', 'v13', 'v21', 'v22', 'v23')
for torsion in element.findall('Torsion'): for torsion in element.findall('Torsion'):
types = forceField._findAtomTypes(torsion.attrib, 4) try:
if None not in types: params = tuple(float(torsion.attrib[p]) for p in ('v11', 'v12', 'v13', 'v21', 'v22', 'v23'))
v = [float(torsion.attrib[param]) for param in params] generator.builder.registerParams((torsion.attrib['class1'], torsion.attrib['class2'], torsion.attrib['class3'], torsion.attrib['class4']), params)
generator.torsions.append((types, v)) except:
outputString = "AmoebaAngleTorsionGenerator: error getting classes: %s %s %s %s" % (
torsion.attrib['class1'],
torsion.attrib['class2'],
torsion.attrib['class3'],
torsion.attrib['class4'])
raise ValueError(outputString)
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
pass pass
...@@ -4314,103 +4012,35 @@ class AmoebaAngleTorsionGenerator(object): ...@@ -4314,103 +4012,35 @@ class AmoebaAngleTorsionGenerator(object):
def postprocessSystem(self, sys, data, args): def postprocessSystem(self, sys, data, args):
# We need to wait until after all angles and torsions have been added before adding the angle-torsions, # We need to wait until after all angles and torsions have been added before adding the angle-torsions,
# since it needs parameters from them. # since it needs parameters from them.
force = self.builder.getForce(sys)
builder = amoebaforces.AmoebaAngleTorsionForceBuilder() self.builder.addAngleTorsions(sys, force, data.atomClasses, data.propers)
force = builder.getForce(sys)
# Record parameters for angles and torsions so we can look them up quickly.
angleForce = [f for f in sys.getForces() if type(f) == mm.CustomAngleForce and f.getName() == 'AmoebaAngle'][0]
inPlaneAngleForce = [f for f in sys.getForces() if type(f) == mm.CustomCompoundBondForce and f.getName() == 'AmoebaInPlaneAngle'][0]
torsionForce = [f for f in sys.getForces() if type(f) == mm.PeriodicTorsionForce][0]
equilAngle = {}
torsionPhase = defaultdict(lambda: [0.0, math.pi, 0.0])
angleScale = math.pi/180
for i in range(angleForce.getNumAngles()):
p1, p2, p3, params = angleForce.getAngleParameters(i)
equilAngle[(p1, p2, p3)] = params[0]*angleScale
equilAngle[(p3, p2, p1)] = params[0]*angleScale
for i in range(inPlaneAngleForce.getNumBonds()):
particles, params = inPlaneAngleForce.getBondParameters(i)
equilAngle[tuple(particles[:3])] = params[0]*angleScale
equilAngle[tuple(reversed(particles[:3]))] = params[0]*angleScale
for i in range(torsionForce.getNumTorsions()):
p1, p2, p3, p4, periodicity, phase, k = torsionForce.getTorsionParameters(i)
if periodicity < 4:
phase = phase.value_in_unit(unit.radian)
torsionPhase[(p1, p2, p3, p4)][periodicity-1] = phase
torsionPhase[(p4, p3, p2, p1)][periodicity-1] = phase
# Add stretch-torsions.
for torsion in data.propers:
type1 = data.atomType[data.atoms[torsion[0]]]
type2 = data.atomType[data.atoms[torsion[1]]]
type3 = data.atomType[data.atoms[torsion[2]]]
type4 = data.atomType[data.atoms[torsion[3]]]
for types, v in self.torsions:
if (type1 in types[3] and type2 in types[2] and type3 in types[1] and type4 in types[0]):
type1, type2, type3, type4 = type4, type3, type2, type1
torsion = tuple(reversed(torsion))
if (type1 in types[0] and type2 in types[1] and type3 in types[2] and type4 in types[3]):
params = list(v)
params.append(equilAngle[(torsion[0], torsion[1], torsion[2])])
params.append(equilAngle[(torsion[1], torsion[2], torsion[3])])
params += torsionPhase[torsion]
force.addBond(torsion, params)
break
parsers["AmoebaAngleTorsionForce"] = AmoebaAngleTorsionGenerator.parseElement parsers["AmoebaAngleTorsionForce"] = AmoebaAngleTorsionGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaTorsionTorsionGenerator(object): class AmoebaTorsionTorsionGenerator(object):
#=============================================================================================
"""An AmoebaTorsionTorsionGenerator constructs a AmoebaTorsionTorsionForce.""" """An AmoebaTorsionTorsionGenerator constructs a AmoebaTorsionTorsionForce."""
#=============================================================================================
def __init__(self): def __init__(self):
self.builder = amoebaforces.AmoebaTorsionTorsionForceBuilder()
self.types1 = []
self.types2 = []
self.types3 = []
self.types4 = []
self.types5 = []
self.gridIndex = []
self.grids = []
#=============================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
generator = AmoebaTorsionTorsionGenerator()
forceField._forces.append(generator)
maxGridIndex = -1
# <AmoebaTorsionTorsionForce > # <AmoebaTorsionTorsionForce >
# <TorsionTorsion class1="3" class2="1" class3="2" class4="3" class5="1" grid="0" nx="25" ny="25" /> # <TorsionTorsion class1="3" class2="1" class3="2" class4="3" class5="1" grid="0" nx="25" ny="25" />
generator = AmoebaTorsionTorsionGenerator()
forceField._forces.append(generator)
# Register torsion-torsion parameters
for torsionTorsion in element.findall('TorsionTorsion'): for torsionTorsion in element.findall('TorsionTorsion'):
types = forceField._findAtomTypes(torsionTorsion.attrib, 5) try:
if None not in types:
generator.types1.append(types[0])
generator.types2.append(types[1])
generator.types3.append(types[2])
generator.types4.append(types[3])
generator.types5.append(types[4])
gridIndex = int(torsionTorsion.attrib['grid']) gridIndex = int(torsionTorsion.attrib['grid'])
if (gridIndex > maxGridIndex): torsionTorsionType = (torsionTorsion.attrib['class1'], torsionTorsion.attrib['class2'],
maxGridIndex = gridIndex torsionTorsion.attrib['class3'], torsionTorsion.attrib['class4'],
torsionTorsion.attrib['class5'])
generator.gridIndex.append(gridIndex) generator.builder.registerParams(torsionTorsionType, gridIndex)
else: except:
outputString = "AmoebaTorsionTorsionGenerator: error getting types: %s %s %s %s %s" % ( outputString = "AmoebaTorsionTorsionGenerator: error getting classes: %s %s %s %s %s" % (
torsionTorsion.attrib['class1'], torsionTorsion.attrib['class1'],
torsionTorsion.attrib['class2'], torsionTorsion.attrib['class2'],
torsionTorsion.attrib['class3'], torsionTorsion.attrib['class3'],
...@@ -4418,38 +4048,28 @@ class AmoebaTorsionTorsionGenerator(object): ...@@ -4418,38 +4048,28 @@ class AmoebaTorsionTorsionGenerator(object):
torsionTorsion.attrib['class5'] ) torsionTorsion.attrib['class5'] )
raise ValueError(outputString) raise ValueError(outputString)
# load grid # Register grid data
# xml source
# <TorsionTorsionGrid grid="0" nx="25" ny="25" > # <TorsionTorsionGrid grid="0" nx="25" ny="25" >
# <Grid angle1="-180.00" angle2="-180.00" f="0.0" fx="2.31064374824e-05" fy="0.0" fxy="-0.0052801799672" /> # <Grid angle1="-180.00" angle2="-180.00" f="0.0" fx="2.31064374824e-05" fy="0.0" fxy="-0.0052801799672" />
# <Grid angle1="-165.00" angle2="-180.00" f="-0.66600912" fx="-0.06983370052" fy="-0.075058725744" fxy="-0.0044462732032" /> # <Grid angle1="-165.00" angle2="-180.00" f="-0.66600912" fx="-0.06983370052" fy="-0.075058725744" fxy="-0.0044462732032" />
# output grid: # Grid format:
# grid[x][y][0] = x value (angle1)
# grid[x][y][1] = y value (angle2)
# grid[x][y][2] = function value (f)
# grid[x][y][3] = dfdx value (fx)
# grid[x][y][4] = dfdy value (fy)
# grid[x][y][5] = dfd(xy) value (fxy)
# grid[x][y][0] = x value
# grid[x][y][1] = y value
# grid[x][y][2] = function value
# grid[x][y][3] = dfdx value
# grid[x][y][4] = dfdy value
# grid[x][y][5] = dfd(xy) value
maxGridIndex += 1
generator.grids = maxGridIndex*[]
for torsionTorsionGrid in element.findall('TorsionTorsionGrid'): for torsionTorsionGrid in element.findall('TorsionTorsionGrid'):
gridIndex = int(torsionTorsionGrid.attrib["grid"])
gridIndex = int(torsionTorsionGrid.attrib[ "grid"]) nx = int(torsionTorsionGrid.attrib["nx"])
nx = int(torsionTorsionGrid.attrib[ "nx"]) ny = int(torsionTorsionGrid.attrib["ny"])
ny = int(torsionTorsionGrid.attrib[ "ny"])
grid = [] grid = []
gridCol = [] gridCol = []
gridColIndex = 0 gridColIndex = 0
for gridEntry in torsionTorsionGrid.findall('Grid'): for gridEntry in torsionTorsionGrid.findall('Grid'):
gridRow = [] gridRow = []
gridRow.append(float(gridEntry.attrib['angle1'])) gridRow.append(float(gridEntry.attrib['angle1']))
gridRow.append(float(gridEntry.attrib['angle2'])) gridRow.append(float(gridEntry.attrib['angle2']))
...@@ -4461,77 +4081,46 @@ class AmoebaTorsionTorsionGenerator(object): ...@@ -4461,77 +4081,46 @@ class AmoebaTorsionTorsionGenerator(object):
gridCol.append(gridRow) gridCol.append(gridRow)
gridColIndex += 1 gridColIndex += 1
if (gridColIndex == nx): if gridColIndex == nx:
grid.append(gridCol) grid.append(gridCol)
gridCol = [] gridCol = []
gridColIndex = 0 gridColIndex = 0
generator.builder.registerGridData(gridIndex, grid)
if (gridIndex == len(generator.grids)):
generator.grids.append(grid)
else:
while(len(generator.grids) < gridIndex):
generator.grids.append([])
generator.grids[gridIndex] = grid
#=============================================================================================
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
builder = amoebaforces.AmoebaTorsionTorsionForceBuilder() force = self.builder.getForce(sys)
force = builder.getForce(sys) masses = [sys.getParticleMass(i).value_in_unit(unit.amu) for i in range(sys.getNumParticles())]
self.builder.addTorsionTorsionInteractions(force, data.propers, data.atomClasses, masses)
# Add torsion-torsion interactions
builder.addTorsionTorsionInteractions(force, data, self.types1, self.types2, self.types3,
self.types4, self.types5, self.gridIndex, sys)
# Set grids
for (index, grid) in enumerate(self.grids):
builder.setTorsionTorsionGrid(force, index, grid)
parsers["AmoebaTorsionTorsionForce"] = AmoebaTorsionTorsionGenerator.parseElement parsers["AmoebaTorsionTorsionForce"] = AmoebaTorsionTorsionGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaStretchBendGenerator(object): class AmoebaStretchBendGenerator(object):
#=============================================================================================
"""An AmoebaStretchBendGenerator constructs a AmoebaStretchBendForce.""" """An AmoebaStretchBendGenerator constructs a AmoebaStretchBendForce."""
#=============================================================================================
def __init__(self, forcefield): def __init__(self, forcefield):
self.forcefield = forcefield self.forcefield = forcefield
self.types1 = [] self.builder = amoebaforces.AmoebaStretchBendForceBuilder()
self.types2 = [] self.stretchBendParams = {}
self.types3 = []
self.k1 = []
self.k2 = []
#=============================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
generator = AmoebaStretchBendGenerator(forceField)
forceField._forces.append(generator)
# <AmoebaStretchBendForce stretchBendUnit="1.0"> # <AmoebaStretchBendForce stretchBendUnit="1.0">
# <StretchBend class1="2" class2="1" class3="3" k1="5.25776946506" k2="5.25776946506" /> # <StretchBend class1="2" class2="1" class3="3" k1="5.25776946506" k2="5.25776946506" />
# <StretchBend class1="2" class2="1" class3="4" k1="3.14005676385" k2="3.14005676385" /> # <StretchBend class1="2" class2="1" class3="4" k1="3.14005676385" k2="3.14005676385" />
generator = AmoebaStretchBendGenerator(forceField)
forceField._forces.append(generator)
stretchBendUnit = float(element.attrib['stretchBendUnit']) if 'stretchBendUnit' in element.attrib else 1.0
for stretchBend in element.findall('StretchBend'): for stretchBend in element.findall('StretchBend'):
types = forceField._findAtomTypes(stretchBend.attrib, 3) try:
if None not in types: class1 = stretchBend.attrib['class1']
class2 = stretchBend.attrib['class2']
generator.types1.append(types[0]) class3 = stretchBend.attrib['class3']
generator.types2.append(types[1]) k1 = float(stretchBend.attrib['k1'])*stretchBendUnit
generator.types3.append(types[2]) k2 = float(stretchBend.attrib['k2'])*stretchBendUnit
generator.stretchBendParams[(class1, class2, class3)] = {'k1': k1, 'k2': k2}
generator.k1.append(float(stretchBend.attrib['k1'])) except:
generator.k2.append(float(stretchBend.attrib['k2'])) outputString = "AmoebaStretchBendGenerator : error getting classes: %s %s %s" % (
else:
outputString = "AmoebaStretchBendGenerator : error getting types: %s %s %s" % (
stretchBend.attrib['class1'], stretchBend.attrib['class1'],
stretchBend.attrib['class2'], stretchBend.attrib['class2'],
stretchBend.attrib['class3']) stretchBend.attrib['class3'])
...@@ -4558,105 +4147,39 @@ class AmoebaStretchBendGenerator(object): ...@@ -4558,105 +4147,39 @@ class AmoebaStretchBendGenerator(object):
#============================================================================================= #=============================================================================================
def createForcePostAmoebaBondForce(self, sys, data, nonbondedMethod, nonbondedCutoff, angleList, args): def createForcePostAmoebaBondForce(self, sys, data, nonbondedMethod, nonbondedCutoff, angles, idealAngles, args):
builder = amoebaforces.AmoebaStretchBendForceBuilder() # Get ideal bond lengths from AmoebaBondForce
force = builder.getForce(sys) bondParams = {(data.atomClasses[bond.atom1], data.atomClasses[bond.atom2]): {"r0": bond.length} for bond in data.bonds}
processedAngles = self.builder.registerAllStretchBendParams(data.atomClasses, angles, self.stretchBendParams, bondParams, idealAngles)
for angleDict in angleList: force = self.builder.getForce(sys)
self.builder.addStretchBends(force, processedAngles)
angle = angleDict['angle']
type1 = data.atomType[data.atoms[angle[0]]]
type2 = data.atomType[data.atoms[angle[1]]]
type3 = data.atomType[data.atoms[angle[2]]]
radian = 57.2957795130
for i in range(len(self.types1)):
types1 = self.types1[i]
types2 = self.types2[i]
types3 = self.types3[i]
# match types
# get ideal bond lengths, bondAB, bondCB
# get ideal angle
if (type2 in types2 and ((type1 in types1 and type3 in types3) or (type3 in types1 and type1 in types3))):
bondAB = -1.0
bondCB = -1.0
swap = 0
for bond in data.atomBonds[angle[1]]:
atom1 = data.bonds[bond].atom1
atom2 = data.bonds[bond].atom2
length = data.bonds[bond].length
if (atom1 == angle[0]):
bondAB = length
if (atom1 == angle[2]):
bondCB = length
if (atom2 == angle[2]):
bondCB = length
if (atom2 == angle[0]):
bondAB = length
# check that ideal angle and bonds are set
if ('idealAngle' not in angleDict):
outputString = "AmoebaStretchBendGenerator: ideal angle is not set for following entry:\n"
outputString += " types: %5s %5s %5s atoms: " % (type1, type2, type3)
outputString += getAtomPrint( data, angle[0] ) + ' '
outputString += getAtomPrint( data, angle[1] ) + ' '
outputString += getAtomPrint( data, angle[2] )
raise ValueError(outputString)
elif (bondAB < 0 or bondCB < 0):
outputString = "AmoebaStretchBendGenerator: bonds not set: %15.7e %15.7e. for following entry:" % (bondAB, bondCB)
outputString += " types: [%5s %5s %5s] atoms: " % (type1, type2, type3)
outputString += getAtomPrint( data, angle[0] ) + ' '
outputString += getAtomPrint( data, angle[1] ) + ' '
outputString += getAtomPrint( data, angle[2] )
raise ValueError(outputString)
else:
if type1 in types1 and type3 in types3:
k1, k2 = self.k1[i], self.k2[i]
else:
k1, k2 = self.k2[i], self.k1[i]
force.addBond((angle[0], angle[1], angle[2]), (bondAB, bondCB, angleDict['idealAngle']/radian, k1, k2))
break
parsers["AmoebaStretchBendForce"] = AmoebaStretchBendGenerator.parseElement parsers["AmoebaStretchBendForce"] = AmoebaStretchBendGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaVdwGenerator(object): class AmoebaVdwGenerator(object):
"""An AmoebaVdwGenerator constructs a AmoebaVdwForce."""
"""A AmoebaVdwGenerator constructs a AmoebaVdwForce."""
#=============================================================================================
def __init__(self, type, radiusrule, radiustype, radiussize, epsilonrule, vdw13Scale, vdw14Scale, vdw15Scale): def __init__(self, type, radiusrule, radiustype, radiussize, epsilonrule, vdw13Scale, vdw14Scale, vdw15Scale):
self.type = type self.builder = amoebaforces.AmoebaVdwForceBuilder(str(type),
self.radiusrule = radiusrule str(radiusrule),
self.radiustype = radiustype str(radiustype),
self.radiussize = radiussize str(radiussize),
self.epsilonrule = epsilonrule str(epsilonrule),
self.vdw13Scale = vdw13Scale float(vdw13Scale),
self.vdw14Scale = vdw14Scale float(vdw14Scale),
self.vdw15Scale = vdw15Scale float(vdw15Scale))
self.params = {}
self.pairs = []
#=============================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <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" >
# <Vdw class="1" sigma="0.371" epsilon="0.46024000000000004" reduction="1.0"/>
# <Vdw class="2" sigma="0.382" epsilon="0.42258400000000007" reduction="1.0"/>
existing = [f for f in forceField._forces if isinstance(f, AmoebaVdwGenerator)] existing = [f for f in forceField._forces if isinstance(f, AmoebaVdwGenerator)]
if len(existing) == 0: if len(existing) == 0:
generator = AmoebaVdwGenerator(element.attrib['type'], element.attrib['radiusrule'], element.attrib['radiustype'], element.attrib['radiussize'], element.attrib['epsilonrule'], generator = AmoebaVdwGenerator(element.attrib['type'], element.attrib['radiusrule'], element.attrib['radiustype'], element.attrib['radiussize'], element.attrib['epsilonrule'],
float(element.attrib['vdw-13-scale']), float(element.attrib['vdw-14-scale']), float(element.attrib['vdw-15-scale'])) element.attrib['vdw-13-scale'], element.attrib['vdw-14-scale'], element.attrib['vdw-15-scale'])
forceField.registerGenerator(generator) forceField.registerGenerator(generator)
else: else:
# Multiple <AmoebaVdwForce> tags were found, probably in different files. Simply add more types to the existing one. # Multiple <AmoebaVdwForce> tags were found, probably in different files. Simply add more types to the existing one.
...@@ -4669,38 +4192,22 @@ class AmoebaVdwGenerator(object): ...@@ -4669,38 +4192,22 @@ class AmoebaVdwGenerator(object):
generator.radiustype != element.attrib['radiustype'] or generator.radiussize != element.attrib['radiussize']: generator.radiustype != element.attrib['radiustype'] or generator.radiussize != element.attrib['radiussize']:
raise ValueError('Found multiple AmoebaVdwForce tags with different combining rules') raise ValueError('Found multiple AmoebaVdwForce tags with different combining rules')
for vdw in element.findall('Vdw'): for vdw in element.findall('Vdw'):
generator.params[vdw.attrib['class']] = tuple(float(vdw.attrib[name]) for name in ('sigma', 'epsilon', 'reduction')) generator.builder.registerClassParams(vdw.attrib['class'], float(vdw.attrib['sigma']), float(vdw.attrib['epsilon']), float(vdw.attrib['reduction']))
for pair in element.findall('Pair'): for pair in element.findall('Pair'):
generator.pairs.append((pair.attrib['class1'], pair.attrib['class2'], float(pair.attrib['sigma']), float(pair.attrib['epsilon']))) generator.builder.registerPairParams(pair.attrib['class1'], pair.attrib['class2'], float(pair.attrib['sigma']), float(pair.attrib['epsilon']))
generator.classNameForType = dict((t.name, int(t.atomClass)) for t in forceField._atomTypes.values())
#=============================================================================================
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
builder = amoebaforces.AmoebaVdwForceBuilder(self.type, self.radiusrule, self.radiustype, self.radiussize, self.epsilonrule, self.vdw13Scale, self.vdw14Scale, self.vdw15Scale) force = self.builder.getForce(sys, nonbondedMethod, args.get('vdwCutoff', nonbondedCutoff), args.get('useDispersionCorrection', True))
for atomClass, params in self.params.items(): self.builder.addParticles(force, data.atomClasses, data.atoms, _findBondsForExclusions(data, sys))
builder.registerClassParams(int(atomClass), *params)
for params in self.pairs:
builder.registerPairParams(int(params[0]), int(params[1]), params[2], params[3])
force = builder.getForce(sys, nonbondedMethod, args.get('vdwCutoff', nonbondedCutoff), args.get('useDispersionCorrection', True))
atomClasses = [self.classNameForType[data.atomType[atom]] for atom in data.atoms]
builder.addParticles(force, atomClasses, data.atoms, _findBondsForExclusions(data, sys))
parsers["AmoebaVdwForce"] = AmoebaVdwGenerator.parseElement parsers["AmoebaVdwForce"] = AmoebaVdwGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaMultipoleGenerator(object): class AmoebaMultipoleGenerator(object):
#=============================================================================================
"""A AmoebaMultipoleGenerator constructs an AmoebaMultipoleForce.""" """A AmoebaMultipoleGenerator constructs an AmoebaMultipoleForce."""
#=============================================================================================
def __init__(self, forceField): def __init__(self, forceField):
self.multipoleType = defaultdict(list) self.builder = amoebaforces.AmoebaMultipoleForceBuilder()
self.polarizationType = {}
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
...@@ -4712,32 +4219,29 @@ class AmoebaMultipoleGenerator(object): ...@@ -4712,32 +4219,29 @@ class AmoebaMultipoleGenerator(object):
# Multiple <AmoebaMultipoleForce> tags were found, probably in different files. Simply add more types to the existing one. # Multiple <AmoebaMultipoleForce> tags were found, probably in different files. Simply add more types to the existing one.
generator = existing[0] generator = existing[0]
# set type map: [ kIndices, multipoles, AMOEBA/OpenMM axis type] # Register multipole parameters using the builder
for atom in element.findall('Multipole'): for atom in element.findall('Multipole'):
types = forceField._findAtomTypes(atom.attrib, 1) types = forceField._findAtomTypes(atom.attrib, 1)
if None not in types: if None not in types:
# k-indices not provided default to 0 # k-indices not provided default to 0
kIndices = [int(atom.attrib['type'])] kIndices = [int(atom.attrib['type'])]
kStrings = [ 'kz', 'kx', 'ky' ] kStrings = [ 'kz', 'kx', 'ky' ]
for kString in kStrings: for kString in kStrings:
kIndices.append(int(atom.attrib.get(kString,0))) kIndices.append(int(atom.attrib.get(kString,0)))
# set multipole # Set multipole
charge = float(atom.attrib['c0']) charge = float(atom.attrib['c0'])
conversion = 1.0 conversion = 1.0
dipole = [ conversion*float(atom.attrib['d1']), conversion*float(atom.attrib['d2']), conversion*float(atom.attrib['d3'])] dipole = [ conversion*float(atom.attrib['d1']), conversion*float(atom.attrib['d2']), conversion*float(atom.attrib['d3'])]
quadrupole = [conversion*float(atom.attrib[a]) for a in ['q11', 'q21', 'q31', 'q21', 'q22', 'q32', 'q31', 'q32', 'q33']] quadrupole = [conversion*float(atom.attrib[a]) for a in ['q11', 'q21', 'q31', 'q21', 'q22', 'q32', 'q31', 'q32', 'q33']]
for t in types[0]: for t in types[0]:
generator.multipoleType[t].append({'kIndices':kIndices, 'charge':charge, 'dipole':dipole, 'quadrupole':quadrupole}) generator.builder.registerMultipoleParams(int(t),
amoebaforces.MultipoleParams(kIndices, charge, dipole, quadrupole))
else: else:
outputString = "AmoebaMultipoleGenerator: error getting type for multipole: %s" % (atom.attrib['class']) outputString = "AmoebaMultipoleGenerator: error getting type for multipole: %s" % (atom.attrib['type'])
raise ValueError(outputString) raise ValueError(outputString)
# polarization parameters # Register polarization parameters using the builder
for atom in element.findall('Polarize'): for atom in element.findall('Polarize'):
types = forceField._findAtomTypes(atom.attrib, 1) types = forceField._findAtomTypes(atom.attrib, 1)
if None not in types: if None not in types:
...@@ -4748,57 +4252,45 @@ class AmoebaMultipoleGenerator(object): ...@@ -4748,57 +4252,45 @@ class AmoebaMultipoleGenerator(object):
pgrp = f'pgrp{index}' pgrp = f'pgrp{index}'
if pgrp in atom.attrib: if pgrp in atom.attrib:
groupTypes.append(int(atom.attrib[pgrp])) groupTypes.append(int(atom.attrib[pgrp]))
for t in types[0]: for t in types[0]:
if t in generator.polarizationType: if int(t) in generator.builder.polarizationParams:
raise ValueError(f'Multipole Polarize tags found for type {t}') raise ValueError(f'Multiple Polarize tags found for type {t}')
generator.polarizationType[t] = {'polarizability': polarizability, 'thole':thole, 'groupTypes':groupTypes} generator.builder.registerPolarizationParams(int(t),
amoebaforces.PolarizationParams(polarizability, thole, groupTypes))
else: else:
outputString = "AmoebaMultipoleGenerator: error getting type for polarize: %s" % (atom.attrib['class']) outputString = "AmoebaMultipoleGenerator: error getting class for polarize: %s" % (atom.attrib.get('class', 'unknown'))
raise ValueError(outputString) raise ValueError(outputString)
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
builder = amoebaforces.AmoebaMultipoleForceBuilder() force = self.builder.getForce(sys, nonbondedMethod, nonbondedCutoff,
for type, params in self.multipoleType.items(): args.get('ewaldErrorTolerance', 1e-4),
for p in params: args.get('polarization', 'mutual'),
builder.registerMultipoleParams(int(type), amoebaforces.MultipoleParams(p['kIndices'], p['charge'], p['dipole'], p['quadrupole'])) args.get('mutualInducedTargetEpsilon', 1e-5),
for type, params in self.polarizationType.items(): args.get('mutualInducedMaxIterations', 60))
builder.registerPolarizationParams(int(type), amoebaforces.PolarizationParams(params['polarizability'], params['thole'], params['groupTypes']))
force = builder.getForce(sys, nonbondedMethod, nonbondedCutoff, args.get('ewaldErrorTolerance', 1e-4), args.get('polarization', 'mutual'),
args.get('mutualInducedTargetEpsilon', 1e-5), args.get('mutualInducedMaxIterations', 60))
atomTypes = [int(data.atomType[atom]) for atom in data.atoms] atomTypes = [int(data.atomType[atom]) for atom in data.atoms]
builder.addMultipoles(force, atomTypes, data.atoms, _findBondsForExclusions(data, sys)) self.builder.addMultipoles(force, atomTypes, data.atoms, _findBondsForExclusions(data, sys))
parsers["AmoebaMultipoleForce"] = AmoebaMultipoleGenerator.parseElement parsers["AmoebaMultipoleForce"] = AmoebaMultipoleGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaWcaDispersionGenerator(object): class AmoebaWcaDispersionGenerator(object):
"""A AmoebaWcaDispersionGenerator constructs a AmoebaWcaDispersionForce.""" """A AmoebaWcaDispersionGenerator constructs a AmoebaWcaDispersionForce."""
#=========================================================================================
def __init__(self, epso, epsh, rmino, rminh, awater, slevy, dispoff, shctd): def __init__(self, epso, epsh, rmino, rminh, awater, slevy, dispoff, shctd):
self.builder = amoebaforces.AmoebaWcaDispersionForceBuilder(float(epso),
self.epso = epso float(epsh),
self.epsh = epsh float(rmino),
self.rmino = rmino float(rminh),
self.rminh = rminh float(awater),
self.awater = awater float(slevy),
self.slevy = slevy float(dispoff),
self.dispoff = dispoff float(shctd))
self.shctd = shctd
self.params = {}
#=========================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <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" > # <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" >
# <WcaDispersion class="1" radius="0.1855" epsilon="0.46024" /> # <WcaDispersion class="1" radius="0.1855" epsilon="0.46024" />
# <WcaDispersion class="2" radius="0.191" epsilon="0.422584" /> # <WcaDispersion class="2" radius="0.191" epsilon="0.422584" />
existing = [f for f in forceField._forces if isinstance(f, AmoebaWcaDispersionGenerator)] existing = [f for f in forceField._forces if isinstance(f, AmoebaWcaDispersionGenerator)]
if len(existing) == 0: if len(existing) == 0:
generator = AmoebaWcaDispersionGenerator(element.attrib['epso'], generator = AmoebaWcaDispersionGenerator(element.attrib['epso'],
...@@ -4815,56 +4307,32 @@ class AmoebaWcaDispersionGenerator(object): ...@@ -4815,56 +4307,32 @@ class AmoebaWcaDispersionGenerator(object):
generator = existing[0] generator = existing[0]
for wca in element.findall('WcaDispersion'): for wca in element.findall('WcaDispersion'):
generator.params[int(wca.attrib['class'])] = tuple(float(wca.attrib[name]) for name in ('radius', 'epsilon')) generator.builder.registerParams(wca.attrib['class'], float(wca.attrib['radius']), float(wca.attrib['epsilon']))
generator.classNameForType = dict((t.name, int(t.atomClass)) for t in forceField._atomTypes.values())
#=========================================================================================
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
builder = amoebaforces.AmoebaWcaDispersionForceBuilder(float(self.epso), force = self.builder.getForce(sys)
float(self.epsh), self.builder.addParticles(force, data.atomClasses)
float(self.rmino),
float(self.rminh),
float(self.awater),
float(self.slevy),
float(self.dispoff),
float(self.shctd))
force = builder.getForce(sys)
atomClasses = [self.classNameForType[data.atomType[atom]] for atom in data.atoms]
for atomClass, params in self.params.items():
builder.registerParams(atomClass, *params)
builder.addParticles(force, atomClasses)
parsers["AmoebaWcaDispersionForce"] = AmoebaWcaDispersionGenerator.parseElement parsers["AmoebaWcaDispersionForce"] = AmoebaWcaDispersionGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaGeneralizedKirkwoodGenerator(object): class AmoebaGeneralizedKirkwoodGenerator(object):
#=========================================================================================
"""A AmoebaGeneralizedKirkwoodGenerator constructs a AmoebaGeneralizedKirkwoodForce.""" """A AmoebaGeneralizedKirkwoodGenerator constructs a AmoebaGeneralizedKirkwoodForce."""
#=========================================================================================
def __init__(self, forceField, solventDielectric, soluteDielectric, includeCavityTerm, probeRadius, surfaceAreaFactor): def __init__(self, forceField, solventDielectric, soluteDielectric, includeCavityTerm, probeRadius, surfaceAreaFactor):
self.builder = amoebaforces.AmoebaGeneralizedKirkwoodForceBuilder(float(solventDielectric),
float(soluteDielectric),
bool(includeCavityTerm),
float(probeRadius),
float(surfaceAreaFactor))
self.forceField = forceField self.forceField = forceField
self.solventDielectric = solventDielectric
self.soluteDielectric = soluteDielectric
self.includeCavityTerm = includeCavityTerm
self.probeRadius = probeRadius
self.surfaceAreaFactor = surfaceAreaFactor
#=========================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <AmoebaGeneralizedKirkwoodForce solventDielectric="78.3" soluteDielectric="1.0" includeCavityTerm="1" probeRadius="0.14" surfaceAreaFactor="-170.351730663"> # <AmoebaGeneralizedKirkwoodForce solventDielectric="78.3" soluteDielectric="1.0" includeCavityTerm="1" probeRadius="0.14" surfaceAreaFactor="-170.351730663">
# <GeneralizedKirkwood type="1" charge="-0.22620" shct="0.79" /> # <GeneralizedKirkwood type="1" charge="-0.22620" shct="0.79" />
# <GeneralizedKirkwood type="2" charge="-0.15245" shct="0.72" /> # <GeneralizedKirkwood type="2" charge="-0.15245" shct="0.72" />
existing = [f for f in forceField._forces if isinstance(f, AmoebaGeneralizedKirkwoodGenerator)] existing = [f for f in forceField._forces if isinstance(f, AmoebaGeneralizedKirkwoodGenerator)]
if len(existing) == 0: if len(existing) == 0:
generator = AmoebaGeneralizedKirkwoodGenerator(forceField, element.attrib['solventDielectric'], generator = AmoebaGeneralizedKirkwoodGenerator(forceField, element.attrib['solventDielectric'],
...@@ -4877,110 +4345,81 @@ class AmoebaGeneralizedKirkwoodGenerator(object): ...@@ -4877,110 +4345,81 @@ class AmoebaGeneralizedKirkwoodGenerator(object):
# Multiple <AmoebaGeneralizedKirkwoodFprce> tags were found, probably in different files. Simply add more types to the existing one. # Multiple <AmoebaGeneralizedKirkwoodFprce> tags were found, probably in different files. Simply add more types to the existing one.
generator = existing[0] generator = existing[0]
#=========================================================================================
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
if( nonbondedMethod != NoCutoff ): if( nonbondedMethod != NoCutoff ):
raise ValueError( "Only the nonbondedMethod=NoCutoff option is available for implicit solvent simulations." ) raise ValueError( "Only the nonbondedMethod=NoCutoff option is available for implicit solvent simulations." )
# check if AmoebaMultipoleForce exists since charges needed # check if AmoebaMultipoleForce exists since charges needed
# if it has not been created, raise an error # if it has not been created, raise an error
amoebaMultipoleForceList = [f for f in sys.getForces() if type(f) == mm.AmoebaMultipoleForce] amoebaMultipoleForceList = [f for f in sys.getForces() if type(f) == mm.AmoebaMultipoleForce]
if (len(amoebaMultipoleForceList) > 0): if (len(amoebaMultipoleForceList) > 0):
amoebaMultipoleForce = amoebaMultipoleForceList[0] amoebaMultipoleForce = amoebaMultipoleForceList[0]
else: else:
# call AmoebaMultipoleForceGenerator.createForce() to ensure charges have been set # call AmoebaMultipoleForceGenerator.createForce() to ensure charges have been set
for force in self.forceField._forces: for force in self.forceField._forces:
if (force.__class__.__name__ == 'AmoebaMultipoleGenerator'): if (force.__class__.__name__ == 'AmoebaMultipoleGenerator'):
force.createForce(sys, data, nonbondedMethod, nonbondedCutoff, args) force.createForce(sys, data, nonbondedMethod, nonbondedCutoff, args)
break
# Use the amoebaforces builder
solventDielectric = args.get('solventDielectric', self.solventDielectric)
soluteDielectric = args.get('soluteDielectric', self.soluteDielectric)
includeCavityTerm = args.get('includeCavityTerm', self.includeCavityTerm)
builder = amoebaforces.AmoebaGeneralizedKirkwoodForceBuilder(float(solventDielectric),
float(soluteDielectric),
int(includeCavityTerm),
float(self.probeRadius),
float(self.surfaceAreaFactor))
# Get the charges from the AmoebaMultipoleForce and register them with the builder # Get the charges from the AmoebaMultipoleForce and register them with the builder
for atomIndex in range(0, amoebaMultipoleForce.getNumMultipoles()): for atomIndex in range(amoebaMultipoleForce.getNumMultipoles()):
multipoleParameters = amoebaMultipoleForce.getMultipoleParameters(atomIndex) multipoleParameters = amoebaMultipoleForce.getMultipoleParameters(atomIndex)
builder.registerParams(multipoleParameters[0]) self.builder.registerParams(multipoleParameters[0])
force = builder.getForce(sys, implicitSolvent=True) force = self.builder.getForce(sys, implicitSolvent=True)
if force is not None: if force is not None:
builder.addParticles(force, data.atoms, _findBondsForExclusions(data, sys)) self.builder.addParticles(force, data.atoms, _findBondsForExclusions(data, sys))
parsers["AmoebaGeneralizedKirkwoodForce"] = AmoebaGeneralizedKirkwoodGenerator.parseElement parsers["AmoebaGeneralizedKirkwoodForce"] = AmoebaGeneralizedKirkwoodGenerator.parseElement
#=============================================================================================
## @private ## @private
class AmoebaUreyBradleyGenerator(object): class AmoebaUreyBradleyGenerator(object):
#=============================================================================================
"""An AmoebaUreyBradleyGenerator constructs a AmoebaUreyBradleyForce.""" """An AmoebaUreyBradleyGenerator constructs a AmoebaUreyBradleyForce."""
#=============================================================================================
def __init__(self): def __init__(self):
self.builder = amoebaforces.AmoebaUreyBradleyForceBuilder()
self.anglesForAtom2Type = defaultdict(list)
self.types1 = []
self.types2 = []
self.types3 = []
self.length = []
self.k = []
#=============================================================================================
@staticmethod @staticmethod
def parseElement(element, forceField): def parseElement(element, forceField):
# <AmoebaUreyBradleyForce> # <AmoebaUreyBradleyForce>
# <UreyBradley class1="74" class2="73" class3="74" k="16003.8" d="0.15537" /> # <UreyBradley class1="74" class2="73" class3="74" k="16003.8" d="0.15537" />
generator = AmoebaUreyBradleyGenerator() generator = AmoebaUreyBradleyGenerator()
forceField._forces.append(generator) forceField._forces.append(generator)
for bond in element.findall('UreyBradley'): for bond in element.findall('UreyBradley'):
types = forceField._findAtomTypes(bond.attrib, 3) try:
if None not in types: isClass = any(k.startswith('class') for k in bond.attrib)
index = len(generator.types1) isType = any(k.startswith('type') for k in bond.attrib)
generator.types1.append(types[0]) if isClass and isType:
generator.types2.append(types[1]) raise ValueError(
generator.types3.append(types[2]) "AmoebaUreyBradleyGenerator: Cannot mix classes and types in the same Urey-Bradley term definition. "
for t in types[1]: "Please contact the developers if you require this functionality."
generator.anglesForAtom2Type[t].append(index) )
generator.length.append(float(bond.attrib['d'])) prefix = 'class' if isClass else 'type'
generator.k.append(float(bond.attrib['k'])) keys = (prefix + "1", prefix + "2", prefix + "3")
k = float(bond.attrib['k'])
#============================================================================================= d = float(bond.attrib['d'])
generator.builder.registerParams(
(bond.attrib[keys[0]], bond.attrib[keys[1]], bond.attrib[keys[2]]),
(k, d),
isClass
)
except Exception as e:
outputString = f"AmoebaUreyBradleyGenerator : error getting {'classes' if isClass else 'types'}: " \
f"{bond.attrib[keys[0]]} {bond.attrib[keys[1]]} {bond.attrib[keys[2]]} " \
f"({e})"
raise ValueError(outputString)
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
builder = amoebaforces.AmoebaUreyBradleyForceBuilder() force = self.builder.getForce(sys)
force = builder.getForce(sys) # Match against parameters registered by class
for (angle, isConstrained) in zip(data.angles, data.isAngleConstrained): self.builder.addUreyBradleys(force, data.atomClasses, data.angles, True, data.isAngleConstrained, args.get('flexibleConstraints', False))
if (isConstrained and not args.get('flexibleConstraints', False)): # Match against parameters registered by type
continue atomTypes = [data.atomType[atom] for atom in data.atoms]
type1 = data.atomType[data.atoms[angle[0]]] self.builder.addUreyBradleys(force, atomTypes, data.angles, False, data.isAngleConstrained, args.get('flexibleConstraints', False))
type2 = data.atomType[data.atoms[angle[1]]]
type3 = data.atomType[data.atoms[angle[2]]]
for i in self.anglesForAtom2Type[type2]:
types1 = self.types1[i]
types2 = self.types2[i]
types3 = self.types3[i]
if ((type1 in types1 and type2 in types2 and type3 in types3) or (type3 in types1 and type2 in types2 and type1 in types3)):
force.addBond(angle[0], angle[2], self.length[i], 2*self.k[i])
break
parsers["AmoebaUreyBradleyForce"] = AmoebaUreyBradleyGenerator.parseElement parsers["AmoebaUreyBradleyForce"] = AmoebaUreyBradleyGenerator.parseElement
#=============================================================================================
## @private ## @private
class HippoNonbondedGenerator(object): class HippoNonbondedGenerator(object):
"""A HippoNonbondedGenerator constructs a HippoNonbondedForce.""" """A HippoNonbondedGenerator constructs a HippoNonbondedForce."""
...@@ -5179,5 +4618,3 @@ class DrudeGenerator(object): ...@@ -5179,5 +4618,3 @@ class DrudeGenerator(object):
sys.setParticleMass(parent, sys.getParticleMass(parent)-transferMass) sys.setParticleMass(parent, sys.getParticleMass(parent)-transferMass)
parsers["DrudeForce"] = DrudeGenerator.parseElement parsers["DrudeForce"] = DrudeGenerator.parseElement
\ No newline at end of file
#=============================================================================================
...@@ -65,13 +65,35 @@ class BaseAmoebaForceBuilder: ...@@ -65,13 +65,35 @@ class BaseAmoebaForceBuilder:
ValueError ValueError
If the atom does not have an associated element or atomic number. If the atom does not have an associated element or atomic number.
""" """
if hasattr(atom, 'element'): if hasattr(atom, 'atomicNumber'):
return atom.element.atomic_number
elif hasattr(atom, 'atomicNumber'):
return atom.atomicNumber return atom.atomicNumber
elif hasattr(atom, 'element'):
return atom.element.atomic_number
else: else:
raise ValueError(f"Atom {atom} does not have an associated element or atomic number.") raise ValueError(f"Atom {atom} does not have an associated element or atomic number.")
@staticmethod
def _getNeighbors(atom: Any) -> List[int]:
"""
Get the indices of neighboring atoms bonded to the given atom.
Parameters
----------
atom : Any
Atom object that has either a bonds attribute or a bondedAtoms attribute.
Returns
-------
List[int]
List of indices of neighboring atoms.
"""
if hasattr(atom, 'bondedAtoms'):
return [a.index for a in atom.bondedAtoms]
elif hasattr(atom, 'bonds'):
return [a for a in atom.bonds]
else:
raise ValueError(f"Atom {atom} does not have bonded atoms information.")
def _findExistingForce(self, sys: mm.System, forceType: type, energyFunction: Optional[str] = None, name: Optional[str] = None) -> Optional[Any]: def _findExistingForce(self, sys: mm.System, forceType: type, energyFunction: Optional[str] = None, name: Optional[str] = None) -> Optional[Any]:
""" """
Find existing force in system by type and optionally by energy function or name. Find existing force in system by type and optionally by energy function or name.
...@@ -178,38 +200,100 @@ class BaseAmoebaForceBuilder: ...@@ -178,38 +200,100 @@ class BaseAmoebaForceBuilder:
class AmoebaBondForceBuilder(BaseAmoebaForceBuilder): class AmoebaBondForceBuilder(BaseAmoebaForceBuilder):
"""Builder for Bond force for AMOEBA force field""" """
Builder for AmoebaBondForce for AMOEBA force field
Attributes
----------
cubic : float
Cubic term coefficient
quartic : float
Quartic term coefficient
bondParams : list
List of bond parameters as tuples of (bondType, (r0, k0)),
where bondType is a tuple of atom classes.
Parameters
----------
cubic : float
Cubic term coefficient
quartic : float
Quartic term coefficient
"""
def __init__(self, cubic, quartic): def __init__(self, cubic: float, quartic: float) -> None:
super().__init__() super().__init__()
self.cubic = cubic self.cubic = cubic
self.quartic = quartic self.quartic = quartic
self.bondParams = [] self.bondParams = []
def registerParams(self, bondType, r0, k0): def registerParams(self, bondType: tuple, r0: float, k0: float) -> None:
"""Register bond parameters""" """
Register bond parameters.
Parameters
----------
bondType : tuple
A tuple of atom classes defining the bond type.
r0 : float
The equilibrium bond length.
k0 : float
The force constant.
"""
self.bondParams.append((bondType, (r0, k0))) self.bondParams.append((bondType, (r0, k0)))
def getForce(self, sys): def getForce(self, sys: mm.System) -> mm.CustomBondForce:
"""
Get or create the AmoebaBondForce in the system.
Parameters
----------
sys : mm.System
The system to get the force from or add it to.
Returns
-------
mm.CustomBondForce
The AmoebaBondForce instance.
"""
energy = f"k*(d^2 + {self.cubic}*d^3 + {self.quartic}*d^4); d=r-r0" energy = f"k*(d^2 + {self.cubic}*d^3 + {self.quartic}*d^4); d=r-r0"
def createForce(): def createForce():
force = mm.CustomBondForce(energy) force = mm.CustomBondForce(energy)
force.addPerBondParameter("r0") force.addPerBondParameter("r0")
force.addPerBondParameter("k") force.addPerBondParameter("k")
force.setName("AmoebaBond") force.setName("AmoebaBondForce")
return force return force
return self._createOrGetForce(sys, mm.CustomBondForce, createForce, energyFunction=energy) return self._createOrGetForce(sys, mm.CustomBondForce, createForce, energyFunction=energy)
def addBonds(self, force, atomClasses, bonds): def addBonds(self, force: mm.CustomBondForce, atomClasses: list, bonds: list, bondsConstraints: Optional[list] = None, flexibleConstraints: bool = False) -> None:
"""Add bonds to the force""" """
for atom1, atom2 in bonds: Add bonds to the AmoebaBondForce.
Parameters
----------
force : mm.CustomBondForce
The AmoebaBondForce instance to add bonds to.
atomClasses : list
List of atom classes indexed by atom index.
bonds : list
List of bonds indices as tuples of (atom1, atom2).
isConstrained : list
List of flags indicating if a given bond is constrainted.
flexibleConstraints : bool
If True, constrained bonds will still be added to the system.
"""
for i, (atom1, atom2) in enumerate(bonds):
isConstrained = False if bondsConstraints is None else bondsConstraints[i]
if not isConstrained or flexibleConstraints:
atomTypes = (atomClasses[atom1], atomClasses[atom2]) atomTypes = (atomClasses[atom1], atomClasses[atom2])
params = self._findMatchingParams(self.bondParams, atomTypes) params = self._findMatchingParams(self.bondParams, atomTypes)
if params is not None and params[1] != 0: if params is not None:
if params[1] != 0.0:
force.addBond(atom1, atom2, params) force.addBond(atom1, atom2, params)
class AmoebaAngleForceBuilder(BaseAmoebaForceBuilder): class AmoebaAngleForceBuilder(BaseAmoebaForceBuilder):
""" """
Builder for AngleForce force for AMOEBA force field. Builder for AngleForce force for AMOEBA force field.
...@@ -289,12 +373,49 @@ class AmoebaAngleForceBuilder(BaseAmoebaForceBuilder): ...@@ -289,12 +373,49 @@ class AmoebaAngleForceBuilder(BaseAmoebaForceBuilder):
force = mm.CustomAngleForce(energy) force = mm.CustomAngleForce(energy)
force.addPerAngleParameter("theta0") force.addPerAngleParameter("theta0")
force.addPerAngleParameter("k") force.addPerAngleParameter("k")
force.setName("AmoebaAngle") force.setName("AmoebaAngleForce")
return force return force
return self._createOrGetForce(sys, mm.CustomAngleForce, createForce, energyFunction=energy) return self._createOrGetForce(sys, mm.CustomAngleForce, createForce, energyFunction=energy)
def addAngles(self, force: mm.CustomAngleForce, angles: List[Tuple[int, int, int]]) -> None: def getIdealAngle(self, angle: Tuple[int, int, int], thetaList: List[float], atoms: List[Any], bondedToAtom: List[Any]) -> Optional[float]:
"""
Get the ideal angle for a given angle.
Notes
-----
Some angle definitions have multiple equilibrium values. For example:
angle 7 7 8 48.20 109.50 110.20 111.00
The appropriate equilibrium angle is determined by the number of hydrogens attached to the central atom that do not participate in the angle.
Parameters
----------
angle : Tuple[int, int, int]
A tuple of atom indices defining the angle.
thetaList : List[float]
List of ideal angles for different k-indices.
atoms : List[Any]
List of atoms indexed by atom index.
bondedToAtom : List[Any]
List of bonded atoms indexed by atom index.
Returns
-------
Optional[float]
The ideal angle if found, None otherwise.
"""
nTheta = len(thetaList)
if nTheta > 1:
partners = [at for at in bondedToAtom[angle[1]] if at not in angle]
nHyd = sum(1 for i in partners if self._getAtomicNumber(atoms[i]) == 1)
if nHyd < nTheta:
theta = thetaList[nHyd]
else:
raise ValueError(f"Angle parameters out of range for angle with indices {angle}")
else:
theta = thetaList[0]
return theta
def addAngles(self, force: mm.CustomAngleForce, angles: List[Tuple[int, int, int]], anglesConstraints: Optional[list] = None, flexibleConstraints: bool = False) -> None:
""" """
Add angles to the force. Add angles to the force.
...@@ -304,20 +425,25 @@ class AmoebaAngleForceBuilder(BaseAmoebaForceBuilder): ...@@ -304,20 +425,25 @@ class AmoebaAngleForceBuilder(BaseAmoebaForceBuilder):
The AmoebaAngleForce instance to add angles to. The AmoebaAngleForce instance to add angles to.
angles : List[Tuple[int, int, int]] angles : List[Tuple[int, int, int]]
List of angle indices as tuples of (atom1, atom2, atom3). List of angle indices as tuples of (atom1, atom2, atom3).
anglesConstraints : Optional[list]
List of flags indicating if a given angle is constrainted.
flexibleConstraints : bool
If True, constrained angles will still be added to the system.
""" """
for atom1, atom2, atom3 in angles: for i, (atom1, atom2, atom3) in enumerate(angles):
for angleType, params in self.angleParams: isConstrained = False if anglesConstraints is None else anglesConstraints[i]
if params[1] != 0: if not isConstrained or flexibleConstraints:
if self._matchParams((atom1, atom2, atom3), angleType): angle = (atom1, atom2, atom3)
params = self._findMatchingParams(self.angleParams, angle)
if params is not None:
if params[1] != 0.0:
force.addAngle(atom1, atom2, atom3, params) force.addAngle(atom1, atom2, atom3, params)
break
class AmoebaInPlaneAngleForceBuilder(BaseAmoebaForceBuilder): class AmoebaInPlaneAngleForceBuilder(BaseAmoebaForceBuilder):
""" """
Builder for InPlaneAngleForce force for AMOEBA force field. Builder for InPlaneAngleForce force for AMOEBA force field.
Attributes Attributes
---------- ----------
cubic : float cubic : float
...@@ -401,12 +527,12 @@ class AmoebaInPlaneAngleForceBuilder(BaseAmoebaForceBuilder): ...@@ -401,12 +527,12 @@ class AmoebaInPlaneAngleForceBuilder(BaseAmoebaForceBuilder):
force = mm.CustomCompoundBondForce(4, energy) force = mm.CustomCompoundBondForce(4, energy)
force.addPerBondParameter("theta0") force.addPerBondParameter("theta0")
force.addPerBondParameter("k") force.addPerBondParameter("k")
force.setName("AmoebaInPlaneAngle") force.setName("AmoebaInPlaneAngleForce")
return force return force
return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy) return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy)
def addInPlaneAngles(self, force: mm.CustomCompoundBondForce, atomClasses: List[Any], inPlaneAngles: List[Tuple[int, int, int, int]]) -> None: def addInPlaneAngles(self, force: mm.CustomCompoundBondForce, atomClasses: List[Any], inPlaneAngles: List[Tuple[int, int, int, int]], anglesConstraints: Optional[list] = None, flexibleConstraints: bool = False) -> None:
""" """
Add in-plane angles to the force. Add in-plane angles to the force.
...@@ -418,14 +544,19 @@ class AmoebaInPlaneAngleForceBuilder(BaseAmoebaForceBuilder): ...@@ -418,14 +544,19 @@ class AmoebaInPlaneAngleForceBuilder(BaseAmoebaForceBuilder):
List of atom classes indexed by atom index. List of atom classes indexed by atom index.
inPlaneAngles : List[Tuple[int, int, int, int]] inPlaneAngles : List[Tuple[int, int, int, int]]
List of in-plane angle indices as tuples of (atom1, atom2, atom3, atom4). List of in-plane angle indices as tuples of (atom1, atom2, atom3, atom4).
anglesConstraints : Optional[list]
List of flags indicating if a given angle is constrainted.
flexibleConstraints : bool
If True, constrained angles will still be added to the system.
""" """
for atom1, atom2, atom3, atom4 in inPlaneAngles: for i, (atom1, atom2, atom3, atom4) in enumerate(inPlaneAngles):
for inPlaneAngleType, params in self.inPlaneAngleParams: isConstrained = False if anglesConstraints is None else anglesConstraints[i]
if params[1] != 0: if not isConstrained or flexibleConstraints:
angleClasses = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3]) angleClasses = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3])
if self._matchParams(angleClasses, inPlaneAngleType[:3], reverseMatch=True): params = self._findMatchingParams(self.inPlaneAngleParams, angleClasses, reverseMatch=True)
if params is not None:
if params[1] != 0.0:
force.addBond([atom1, atom2, atom3, atom4], params) force.addBond([atom1, atom2, atom3, atom4], params)
break
class AmoebaOutOfPlaneBendForceBuilder(BaseAmoebaForceBuilder): class AmoebaOutOfPlaneBendForceBuilder(BaseAmoebaForceBuilder):
...@@ -486,7 +617,91 @@ class AmoebaOutOfPlaneBendForceBuilder(BaseAmoebaForceBuilder): ...@@ -486,7 +617,91 @@ class AmoebaOutOfPlaneBendForceBuilder(BaseAmoebaForceBuilder):
""" """
at1, at2, at3, at4 = atomClasses at1, at2, at3, at4 = atomClasses
p1, p2, p3, p4 = paramClasses p1, p2, p3, p4 = paramClasses
return at2 == p2 and at4 == p1 and {at1, at3} == {at1 if p3 == "0" else p3, at3 if p4 == "0" else p4} return at2 == p2 and at4 == p1 and {at1, at3} == {at1 if p3 == '' else p3, at3 if p4 == '' else p4}
@staticmethod
def classifyAngles(angles: List[Tuple[int, int, int]], atomClasses: List[Any],
bondedToAtom: List[List[int]], opbendTypes: List[Tuple[Any, ...]]) -> Tuple[
List[Tuple[int, ...]],
List[Tuple[int, int, int, int]],
List[Tuple[int, int, int]]]:
"""
Classify angles into in-plane, out-of-plane, and generic categories.
Notes
-----
For atoms with covalency 3, if the central atom and all partners match
out-of-plane bend parameters, the angles are classified as in-plane
with corresponding out-of-plane bends.
Parameters
----------
angles : List[Tuple[int, int, int]]
List of angles as tuples of (atom1, central_atom, atom2).
atomClasses : List[Any]
List of atom classes indexed by atom index.
bondedToAtom : List[List[int]]
List of bonded atoms for each atom index.
opbendTypes : List[Tuple[Any, ...]]
List of out-of-plane bend parameter types as (type1, type2, type3, type4).
Returns
-------
Tuple[List[Tuple[int, ...]], List[Tuple[int, int, int, int]], List[Tuple[int, int, int]]]
A tuple containing:
- inPlaneAngles: List of in-plane angles (atom1, central, atom2) or (atom1, central, atom2, atom3)
- outOfPlaneAngles: List of out-of-plane bends (atom1, central, atom3, atom4)
- genericAngles: List of generic angles (atom1, central, atom2)
"""
inPlaneAngles = []
outOfPlaneAngles = []
genericAngles = []
skipAtoms = {}
for angle in angles:
middleAtom = angle[1]
middleClass = atomClasses[middleAtom]
middleCovalency = len(bondedToAtom[middleAtom])
if middleCovalency == 3 and middleAtom not in skipAtoms:
partners = []
for partner in bondedToAtom[middleAtom]:
partnerClass = atomClasses[partner]
for opBendType in opbendTypes:
if middleClass in opBendType[1:2] and partnerClass in opBendType[0:1]:
partners.append(partner)
break
if len(partners) == 3:
outOfPlaneAngles.append((partners[0], middleAtom, partners[1], partners[2]))
outOfPlaneAngles.append((partners[2], middleAtom, partners[0], partners[1]))
outOfPlaneAngles.append((partners[1], middleAtom, partners[2], partners[0]))
skipAtoms[middleAtom] = set(partners[:3])
angleList = list(angle[:3])
for atomIndex in partners:
if atomIndex not in angleList:
angleList.append(atomIndex)
inPlaneAngles.append(tuple(angleList))
else:
angleList = list(angle[:3])
for atomIndex in partners:
if atomIndex not in angleList:
angleList.append(atomIndex)
genericAngles.append(tuple(angleList))
elif middleCovalency == 3 and middleAtom in skipAtoms:
angleList = list(angle[:3])
for atomIndex in skipAtoms[middleAtom]:
if atomIndex not in angleList:
angleList.append(atomIndex)
inPlaneAngles.append(tuple(angleList))
else:
genericAngles.append(angle)
return inPlaneAngles, outOfPlaneAngles, genericAngles
def registerParams(self, outOfPlaneBendType: Tuple[Any, ...], params: Tuple[float, ...]) -> None: def registerParams(self, outOfPlaneBendType: Tuple[Any, ...], params: Tuple[float, ...]) -> None:
""" """
...@@ -534,7 +749,7 @@ class AmoebaOutOfPlaneBendForceBuilder(BaseAmoebaForceBuilder): ...@@ -534,7 +749,7 @@ class AmoebaOutOfPlaneBendForceBuilder(BaseAmoebaForceBuilder):
def createForce(): def createForce():
force = mm.CustomCompoundBondForce(4, energy) force = mm.CustomCompoundBondForce(4, energy)
force.addPerBondParameter("k") force.addPerBondParameter("k")
force.setName("AmoebaOutOfPlaneBend") force.setName("AmoebaOutOfPlaneBendForce")
return force return force
return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy) return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy)
...@@ -554,9 +769,9 @@ class AmoebaOutOfPlaneBendForceBuilder(BaseAmoebaForceBuilder): ...@@ -554,9 +769,9 @@ class AmoebaOutOfPlaneBendForceBuilder(BaseAmoebaForceBuilder):
""" """
for atom1, atom2, atom3, atom4 in outOfPlaneBends: for atom1, atom2, atom3, atom4 in outOfPlaneBends:
for outOfPlaneBendType, params in self.outOfPlaneBendParams: for outOfPlaneBendType, params in self.outOfPlaneBendParams:
if params[0] != 0:
angleClasses = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3], atomClasses[atom4]) angleClasses = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3], atomClasses[atom4])
if self._matchParams(angleClasses, outOfPlaneBendType): if self._matchParams(angleClasses, outOfPlaneBendType):
if params[0] != 0.0:
force.addBond([atom1, atom2, atom3, atom4], params) force.addBond([atom1, atom2, atom3, atom4], params)
break break
...@@ -568,11 +783,112 @@ class AmoebaStretchBendForceBuilder(BaseAmoebaForceBuilder): ...@@ -568,11 +783,112 @@ class AmoebaStretchBendForceBuilder(BaseAmoebaForceBuilder):
super().__init__() super().__init__()
self.stretchBendParams = [] self.stretchBendParams = []
def registerParams(self, stretchBendType, params): def registerParams(self, stretchBendType: tuple, params: tuple) -> None:
"""Register stretch-bend parameters""" """
Register stretch-bend parameters.
Parameters
----------
stretchBendType : tuple
A tuple identifying the type of stretch-bend interaction.
params : tuple
A tuple containing the parameters for the stretch-bend interaction.
"""
self.stretchBendParams.append((stretchBendType, params)) self.stretchBendParams.append((stretchBendType, params))
def getForce(self, sys): def registerAllStretchBendParams(self,
atomClasses: list,
angles: list,
stretchBendParams: dict,
bondParams: dict,
idealAngles: dict
):
"""
Register all stretch-bend parameters for the given angles.
Notes
-----
For each angle defined by three atoms (1-2-3), this method checks if there are
corresponding stretch-bend parameters for the atom classes of these atoms. If such
parameters exist, it retrieves the ideal angle and bond parameters for the bonds
(1-2) and (2-3). It then registers the parameters for the stretch-bend interaction.
The method returns a list of angles for which stretch-bend parameters were successfully
registered.
Parameters
----------
atomClasses : list
List of atom classes indexed by atom index.
angles : list
List of angles defined by tuples of (atom1, atom2, atom3).
stretchBendParams : dict
Dictionary mapping atom class triplets to stretch-bend parameters.
bondParams : dict
Dictionary mapping atom class pairs to bond parameters.
idealAngles : dict
Dictionary mapping angle tuples to ideal angles.
Returns
-------
list
List of angles for which stretch-bend parameters were registered.
"""
processedAngles = []
for angle in angles:
class1 = atomClasses[angle[0]]
class2 = atomClasses[angle[1]]
class3 = atomClasses[angle[2]]
params = stretchBendParams.get((class1, class2, class3))
if params is not None:
swap = False
else:
params = stretchBendParams.get((class3, class2, class1))
swap = params is not None
if params:
idealAngle = idealAngles.get(tuple(angle), None)
if idealAngle is None:
continue
if not swap:
# 1-2-3
bondAB = bondParams.get((class1, class2)) or bondParams.get((class2, class1))
bondCB = bondParams.get((class2, class3)) or bondParams.get((class3, class2))
if bondAB and bondCB:
bondAB, bondCB = bondAB['r0'], bondCB['r0']
k1, k2 = params["k1"], params["k2"]
else:
# 3-2-1
bondAB = bondParams.get((class3, class2)) or bondParams.get((class2, class3))
bondCB = bondParams.get((class2, class1)) or bondParams.get((class1, class2))
if bondAB and bondCB:
bondAB, bondCB = bondCB['r0'], bondAB['r0']
k1, k2 = params["k2"], params["k1"]
if bondAB and bondCB:
# Because for the same class triplet, depending on the number of hydrogens on the central atom,
# the ideal angle can be different, we need to register parameters for each angle.
paramKey = (angle[0], angle[1], angle[2])
self.registerParams(paramKey, (bondAB, bondCB, idealAngle, k1, k2))
processedAngles.append((angle[0], angle[1], angle[2]))
return processedAngles
def getForce(self, sys: mm.System) -> mm.CustomCompoundBondForce:
"""
Get the force for the system.
Parameters
----------
sys : mm.System
The system to get the force for.
Returns
-------
mm.CustomCompoundBondForce
The force for the system.
"""
energy = ( energy = (
"(k1*(distance(p1,p2)-r12) + k2*(distance(p2,p3)-r23))*(%.15g*(angle(p1,p2,p3)-theta0))" "(k1*(distance(p1,p2)-r12) + k2*(distance(p2,p3)-r23))*(%.15g*(angle(p1,p2,p3)-theta0))"
% (180 / math.pi) % (180 / math.pi)
...@@ -585,19 +901,28 @@ class AmoebaStretchBendForceBuilder(BaseAmoebaForceBuilder): ...@@ -585,19 +901,28 @@ class AmoebaStretchBendForceBuilder(BaseAmoebaForceBuilder):
force.addPerBondParameter("theta0") force.addPerBondParameter("theta0")
force.addPerBondParameter("k1") force.addPerBondParameter("k1")
force.addPerBondParameter("k2") force.addPerBondParameter("k2")
force.setName("AmoebaStretchBend") force.setName("AmoebaStretchBendForce")
return force return force
return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy) return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy)
def addStretchBends(self, force, angles): def addStretchBends(self, force: mm.CustomCompoundBondForce, angles: list) -> None:
"""Add stretch-bend terms to the force""" """
Add stretch-bend terms to the force
Parameters
----------
force : mm.CustomCompoundBondForce
The force to add the stretch-bend terms to.
angles : list of tuples
The angles to add the stretch-bend terms for.
"""
for atom1, atom2, atom3 in angles: for atom1, atom2, atom3 in angles:
for stretchBendType, params in self.stretchBendParams:
angle = (atom1, atom2, atom3) angle = (atom1, atom2, atom3)
if self._matchParams(angle, stretchBendType, reverseMatch=True): params = self._findMatchingParams(self.stretchBendParams, (atom1, atom2, atom3), reverseMatch=True)
if params[3] != 0.0 or params[4] != 0.0:
force.addBond(angle, params) force.addBond(angle, params)
break
class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder): class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder):
""" """
...@@ -661,7 +986,7 @@ class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -661,7 +986,7 @@ class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder):
force.addPerBondParameter(f'length{i+1}') force.addPerBondParameter(f'length{i+1}')
for i in range(3): for i in range(3):
force.addPerBondParameter(f'phase{i+1}') force.addPerBondParameter(f'phase{i+1}')
force.setName('AmoebaStretchTorsion') force.setName('AmoebaStretchTorsionForce')
return force return force
return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy) return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy)
...@@ -681,10 +1006,8 @@ class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -681,10 +1006,8 @@ class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder):
torsions : List[Tuple[int, int, int, int]] torsions : List[Tuple[int, int, int, int]]
List of torsion indices as tuples of (atom1, atom2, atom3, atom4). List of torsion indices as tuples of (atom1, atom2, atom3, atom4).
""" """
# Record parameters for bonds and torsions so we can look them up quickly. # Record parameters for bonds and torsions so we can look them up quickly.
bondForce = [f for f in sys.getForces() if type(f) == mm.CustomBondForce and f.getName() == 'AmoebaBondForce'][0]
bondForce = [f for f in sys.getForces() if type(f) == mm.CustomBondForce and f.getName() == 'AmoebaBond'][0]
torsionForce = [f for f in sys.getForces() if type(f) == mm.PeriodicTorsionForce][0] torsionForce = [f for f in sys.getForces() if type(f) == mm.PeriodicTorsionForce][0]
bondLength = {} bondLength = {}
torsionPhase = defaultdict(lambda: [0.0, math.pi, 0.0]) torsionPhase = defaultdict(lambda: [0.0, math.pi, 0.0])
...@@ -700,7 +1023,6 @@ class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -700,7 +1023,6 @@ class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder):
torsionPhase[(p4, p3, p2, p1)][periodicity-1] = phase torsionPhase[(p4, p3, p2, p1)][periodicity-1] = phase
# Add stretch-torsions. # Add stretch-torsions.
for torsion in torsions: for torsion in torsions:
atom1, atom2, atom3, atom4 = torsion atom1, atom2, atom3, atom4 = torsion
atomTypes = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3], atomClasses[atom4]) atomTypes = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3], atomClasses[atom4])
...@@ -713,6 +1035,7 @@ class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -713,6 +1035,7 @@ class AmoebaStretchTorsionForceBuilder(BaseAmoebaForceBuilder):
params.append(bondLength[(atom2, atom3)]) params.append(bondLength[(atom2, atom3)])
params.append(bondLength[(atom3, atom4)]) params.append(bondLength[(atom3, atom4)])
params += torsionPhase[torsion] params += torsionPhase[torsion]
if any(p != 0.0 for p in params[:9]):
force.addBond((atom1, atom2, atom3, atom4), params) force.addBond((atom1, atom2, atom3, atom4), params)
break break
...@@ -776,7 +1099,7 @@ class AmoebaAngleTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -776,7 +1099,7 @@ class AmoebaAngleTorsionForceBuilder(BaseAmoebaForceBuilder):
force.addPerBondParameter(f'angle{i+1}') force.addPerBondParameter(f'angle{i+1}')
for i in range(3): for i in range(3):
force.addPerBondParameter(f'phase{i+1}') force.addPerBondParameter(f'phase{i+1}')
force.setName('AmoebaAngleTorsion') force.setName('AmoebaAngleTorsionForce')
return force return force
return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy) return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy)
...@@ -796,11 +1119,9 @@ class AmoebaAngleTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -796,11 +1119,9 @@ class AmoebaAngleTorsionForceBuilder(BaseAmoebaForceBuilder):
torsions : List[Tuple[int, int, int, int]] torsions : List[Tuple[int, int, int, int]]
List of torsion indices as tuples of (atom1, atom2, atom3, atom4). List of torsion indices as tuples of (atom1, atom2, atom3, atom4).
""" """
# Record parameters for angles and torsions so we can look them up quickly. # Record parameters for angles and torsions so we can look them up quickly.
angleForce = [f for f in sys.getForces() if type(f) == mm.CustomAngleForce and f.getName() == 'AmoebaAngleForce'][0]
angleForce = [f for f in sys.getForces() if type(f) == mm.CustomAngleForce and f.getName() == 'AmoebaAngle'][0] inPlaneAngleForce = [f for f in sys.getForces() if type(f) == mm.CustomCompoundBondForce and f.getName() == 'AmoebaInPlaneAngleForce'][0]
inPlaneAngleForce = [f for f in sys.getForces() if type(f) == mm.CustomCompoundBondForce and f.getName() == 'AmoebaInPlaneAngle'][0]
torsionForce = [f for f in sys.getForces() if type(f) == mm.PeriodicTorsionForce][0] torsionForce = [f for f in sys.getForces() if type(f) == mm.PeriodicTorsionForce][0]
equilAngle = {} equilAngle = {}
torsionPhase = defaultdict(lambda: [0.0, math.pi, 0.0]) torsionPhase = defaultdict(lambda: [0.0, math.pi, 0.0])
...@@ -821,7 +1142,6 @@ class AmoebaAngleTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -821,7 +1142,6 @@ class AmoebaAngleTorsionForceBuilder(BaseAmoebaForceBuilder):
torsionPhase[(p4, p3, p2, p1)][periodicity-1] = phase torsionPhase[(p4, p3, p2, p1)][periodicity-1] = phase
# Add angle-torsions. # Add angle-torsions.
for torsion in torsions: for torsion in torsions:
atom1, atom2, atom3, atom4 = torsion atom1, atom2, atom3, atom4 = torsion
atomTypes = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3], atomClasses[atom4]) atomTypes = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3], atomClasses[atom4])
...@@ -833,6 +1153,7 @@ class AmoebaAngleTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -833,6 +1153,7 @@ class AmoebaAngleTorsionForceBuilder(BaseAmoebaForceBuilder):
params.append(equilAngle[(atom1, atom2, atom3)]) params.append(equilAngle[(atom1, atom2, atom3)])
params.append(equilAngle[(atom2, atom3, atom4)]) params.append(equilAngle[(atom2, atom3, atom4)])
params += torsionPhase[torsion] params += torsionPhase[torsion]
if any(p != 0.0 for p in params[:6]):
force.addBond((atom1, atom2, atom3, atom4), params) force.addBond((atom1, atom2, atom3, atom4), params)
break break
...@@ -895,10 +1216,9 @@ class AmoebaTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -895,10 +1216,9 @@ class AmoebaTorsionForceBuilder(BaseAmoebaForceBuilder):
List of torsion indices as tuples of (atom1, atom2, atom3, atom4). List of torsion indices as tuples of (atom1, atom2, atom3, atom4).
""" """
for atom1, atom2, atom3, atom4 in torsions: for atom1, atom2, atom3, atom4 in torsions:
for torsionType, params in self.torsionParams: torsionType = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3], atomClasses[atom4])
atomTypes = (atomClasses[atom1], atomClasses[atom2], params = self._findMatchingParams(self.torsionParams, torsionType)
atomClasses[atom3], atomClasses[atom4]) if params is not None:
if self._matchParams(atomTypes, torsionType):
t1, t2, t3 = params t1, t2, t3 = params
if t1[0] != 0: if t1[0] != 0:
force.addTorsion(atom1, atom2, atom3, atom4, 1, t1[1], t1[0]) force.addTorsion(atom1, atom2, atom3, atom4, 1, t1[1], t1[0])
...@@ -906,7 +1226,7 @@ class AmoebaTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -906,7 +1226,7 @@ class AmoebaTorsionForceBuilder(BaseAmoebaForceBuilder):
force.addTorsion(atom1, atom2, atom3, atom4, 2, t2[1], t2[0]) force.addTorsion(atom1, atom2, atom3, atom4, 2, t2[1], t2[0])
if t3[0] != 0: if t3[0] != 0:
force.addTorsion(atom1, atom2, atom3, atom4, 3, t3[1], t3[0]) force.addTorsion(atom1, atom2, atom3, atom4, 3, t3[1], t3[0])
break continue
class AmoebaPiTorsionForceBuilder(BaseAmoebaForceBuilder): class AmoebaPiTorsionForceBuilder(BaseAmoebaForceBuilder):
...@@ -963,11 +1283,47 @@ class AmoebaPiTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -963,11 +1283,47 @@ class AmoebaPiTorsionForceBuilder(BaseAmoebaForceBuilder):
def createForce(): def createForce():
force = mm.CustomCompoundBondForce(6, energy) force = mm.CustomCompoundBondForce(6, energy)
force.addPerBondParameter("k") force.addPerBondParameter("k")
force.setName("AmoebaPiTorsion") force.setName("AmoebaPiTorsionForce")
return force return force
return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy) return self._createOrGetForce(sys, mm.CustomCompoundBondForce, createForce, energyFunction=energy)
def getAllPiTorsions(self, atomClasses: List[str], bondedToAtom: List[Tuple[int, ...]], bonds: List[Tuple[int, int]]) -> List[Tuple[int, int, int, int, int, int]]:
"""
Get all pi-torsion indices from the bonds.
Parameters
----------
atomClasses : List[str]
List of atom classes indexed by atom index.
bondedToAtom : List[Any]
List of atoms bonded to each atom.
bonds : List[Tuple[int, int]]
List of bonds defined by tuples of (atom1, atom2).
Returns
-------
List[Tuple[int, int, int, int, int, int]]
List of pi-torsion indices as tuples of six atoms.
"""
processedPiTorsions = []
for (at1, at2) in bonds:
valence1 = len(bondedToAtom[at1])
valence2 = len(bondedToAtom[at2])
if valence1 == 3 and valence2 == 3:
class1 = atomClasses[at1]
class2 = atomClasses[at2]
params = self._findMatchingParams(self.piTorsionParams, (class1, class2))
if params:
piTorsionAtom3 = at1
piTorsionAtom4 = at2
# piTorsionAtom1, piTorsionAtom2 are the atoms bonded to atom1, excluding atom2
# piTorsionAtom5, piTorsionAtom6 are the atoms bonded to atom2, excluding atom1
piTorsionAtom1, piTorsionAtom2 = [at for at in bondedToAtom[at1] if at != piTorsionAtom4]
piTorsionAtom5, piTorsionAtom6 = [at for at in bondedToAtom[at2] if at != piTorsionAtom3]
processedPiTorsions.append((piTorsionAtom1, piTorsionAtom2, piTorsionAtom3, piTorsionAtom4, piTorsionAtom5, piTorsionAtom6))
return processedPiTorsions
def addPiTorsions(self, force: mm.CustomCompoundBondForce, atomClasses: List[Any], piTorsions: List[Tuple[int, int, int, int, int, int]]) -> None: def addPiTorsions(self, force: mm.CustomCompoundBondForce, atomClasses: List[Any], piTorsions: List[Tuple[int, int, int, int, int, int]]) -> None:
""" """
Add pi-torsions to the force. Add pi-torsions to the force.
...@@ -982,12 +1338,11 @@ class AmoebaPiTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -982,12 +1338,11 @@ class AmoebaPiTorsionForceBuilder(BaseAmoebaForceBuilder):
List of pi-torsion indices as tuples of six atoms. List of pi-torsion indices as tuples of six atoms.
""" """
for atom1, atom2, atom3, atom4, atom5, atom6 in piTorsions: for atom1, atom2, atom3, atom4, atom5, atom6 in piTorsions:
for piTorsionType, params in self.piTorsionParams: piTorsionType = (atomClasses[atom3], atomClasses[atom4])
if params[0] != 0: params = self._findMatchingParams(self.piTorsionParams, piTorsionType)
types = (atomClasses[atom3], atomClasses[atom4]) if params is not None:
if self._matchParams(types, piTorsionType, reverseMatch=True): if params[0] != 0.0:
force.addBond([atom1, atom2, atom3, atom4, atom5, atom6], params) force.addBond([atom1, atom2, atom3, atom4, atom5, atom6], params)
break
class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder): class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder):
...@@ -1003,9 +1358,9 @@ class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder): ...@@ -1003,9 +1358,9 @@ class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the AmoebaUreyBradleyForceBuilder.""" """Initialize the AmoebaUreyBradleyForceBuilder."""
super().__init__() super().__init__()
self.ureyBradleyParams = [] self.ureyBradleyParams = {True: [], False: []} # True = class-based, False = type-based
def registerParams(self, ureyBradleyType: Tuple[Any, ...], params: Tuple[float, float]) -> None: def registerParams(self, ureyBradleyType: Tuple[Any, ...], params: Tuple[float, float], isClass: bool) -> None:
""" """
Register Urey-Bradley parameters. Register Urey-Bradley parameters.
...@@ -1015,8 +1370,10 @@ class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder): ...@@ -1015,8 +1370,10 @@ class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder):
A tuple of atom classes defining the Urey-Bradley type. A tuple of atom classes defining the Urey-Bradley type.
params : Tuple[float, float] params : Tuple[float, float]
The Urey-Bradley parameters (force constant, equilibrium distance). The Urey-Bradley parameters (force constant, equilibrium distance).
isClass : bool
Indicates if these parameters are defined for atom classes (True) or atom types (False).
""" """
self.ureyBradleyParams.append((ureyBradleyType, params)) self.ureyBradleyParams[isClass].append((ureyBradleyType, params))
def getForce(self, sys: mm.System) -> mm.HarmonicBondForce: def getForce(self, sys: mm.System) -> mm.HarmonicBondForce:
""" """
...@@ -1034,7 +1391,7 @@ class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder): ...@@ -1034,7 +1391,7 @@ class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder):
""" """
return self._createOrGetForce(sys, mm.HarmonicBondForce, mm.HarmonicBondForce) return self._createOrGetForce(sys, mm.HarmonicBondForce, mm.HarmonicBondForce)
def addUreyBradleys(self, force: mm.HarmonicBondForce, atomClasses: List[Any], angles: List[Tuple[int, int, int]], anglesConstraints: Optional[List[bool]] = None, flexibleConstraints: bool = False) -> None: def addUreyBradleys(self, force: mm.HarmonicBondForce, atomClassesOrTypes: List[Any], angles: List[Tuple[int, int, int]], isClass: bool, anglesConstraints: Optional[List[bool]] = None, flexibleConstraints: bool = False) -> None:
""" """
Add Urey-Bradley terms to the force. Add Urey-Bradley terms to the force.
...@@ -1042,42 +1399,88 @@ class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder): ...@@ -1042,42 +1399,88 @@ class AmoebaUreyBradleyForceBuilder(BaseAmoebaForceBuilder):
---------- ----------
force : mm.HarmonicBondForce force : mm.HarmonicBondForce
The HarmonicBondForce instance to add Urey-Bradley terms to. The HarmonicBondForce instance to add Urey-Bradley terms to.
atomClasses : List[Any] atomClassesOrTypes : List[Any]
List of atom classes indexed by atom index. List of atom classes or types indexed by atom index.
angles : List[Tuple[int, int, int]] angles : List[Tuple[int, int, int]]
List of angle indices as tuples of (atom1, atom2, atom3). List of angle indices as tuples of (atom1, atom2, atom3).
isClass : bool
Indicates if the parameters are defined for atom classes (True) or atom types (False).
anglesConstraints : Optional[List[bool]], default=None anglesConstraints : Optional[List[bool]], default=None
List of flags indicating if a given angle is constrained. List of flags indicating if a given angle is constrained.
flexibleConstraints : bool, default=False flexibleConstraints : bool, default=False
If True, constrained angles will still be added to the system. If True, constrained angles will still be added to the system.
""" """
paramSet = self.ureyBradleyParams[isClass]
for i, (atom1, atom2, atom3) in enumerate(angles): for i, (atom1, atom2, atom3) in enumerate(angles):
isConstrained = False if anglesConstraints is None else anglesConstraints[i] isConstrained = False if anglesConstraints is None else anglesConstraints[i]
if not isConstrained or flexibleConstraints: if not isConstrained or flexibleConstraints:
ubType = (atomClasses[atom1], atomClasses[atom2], atomClasses[atom3]) ubIdentifier = (atomClassesOrTypes[atom1], atomClassesOrTypes[atom2], atomClassesOrTypes[atom3])
params = self._findMatchingParams(self.ureyBradleyParams, ubType, reverseMatch=True) params = self._findMatchingParams(paramSet, ubIdentifier, reverseMatch=True)
if params is not None: if params is not None:
k, d = params k, d = params
if k != 0.0:
force.addBond(atom1, atom3, d, 2 * k) force.addBond(atom1, atom3, d, 2 * k)
class AmoebaTorsionTorsionForceBuilder(BaseAmoebaForceBuilder): class AmoebaTorsionTorsionForceBuilder(BaseAmoebaForceBuilder):
"""Builder for TorsionTorsion force for AMOEBA force field""" """
Builder for TorsionTorsion force for AMOEBA force field.
def __init__(self):
Attributes
----------
torsionTorsionParams : List[Tuple[Tuple[str, ...], int]]
List of registered torsion-torsion parameters as (atom_class_pattern, grid_index) tuples.
gridData : Dict[int, Any]
Dictionary mapping grid indices to grid data for torsion-torsion interactions.
"""
def __init__(self) -> None:
"""Initialize the AmoebaTorsionTorsionForceBuilder."""
super().__init__() super().__init__()
self.torsionTorsionParams = [] self.torsionTorsionParams: List[Tuple[Tuple[str, ...], int]] = []
self.gridData = {} self.gridData: Dict[int, Any] = {}
def registerParams(self, torsionTorsionType, params): def registerParams(self, torsionTorsionType: Tuple[str, ...], params: int) -> None:
"""Register torsion-torsion parameters""" """
Register torsion-torsion parameters during XML parsing.
Parameters
----------
torsionTorsionType : Tuple[str, ...]
Tuple of atom class strings defining the torsion-torsion type pattern.
params : int
Grid index for the torsion-torsion interaction.
"""
self.torsionTorsionParams.append((torsionTorsionType, params)) self.torsionTorsionParams.append((torsionTorsionType, params))
def registerGridData(self, gridIndex, grid): def registerGridData(self, gridIndex: int, grid: Any) -> None:
"""Register grid data for torsion-torsion interactions""" """
Register grid data for torsion-torsion interactions.
Parameters
----------
gridIndex : int
Index of the grid in the force.
grid : Any
Grid data object for the torsion-torsion interaction.
"""
self.gridData[gridIndex] = grid self.gridData[gridIndex] = grid
def getForce(self, sys): def getForce(self, sys: mm.System) -> mm.AmoebaTorsionTorsionForce:
"""
Get or create the AmoebaTorsionTorsionForce in the system.
Parameters
----------
sys : mm.System
The OpenMM System to get the force from or add it to.
Returns
-------
mm.AmoebaTorsionTorsionForce
The AmoebaTorsionTorsionForce instance with grid data registered.
"""
def createForce(): def createForce():
force = mm.AmoebaTorsionTorsionForce() force = mm.AmoebaTorsionTorsionForce()
return force return force
...@@ -1089,245 +1492,106 @@ class AmoebaTorsionTorsionForceBuilder(BaseAmoebaForceBuilder): ...@@ -1089,245 +1492,106 @@ class AmoebaTorsionTorsionForceBuilder(BaseAmoebaForceBuilder):
return force return force
@staticmethod def addTorsionTorsionInteractions(self, force: mm.AmoebaTorsionTorsionForce, torsions: List[Tuple[int, ...]], atomClasses: List[str], masses: List[float]) -> None:
def setTorsionTorsionGrid(force, gridIndex, grid):
"""Set the torsion-torsion grid for the force."""
force.setTorsionTorsionGrid(gridIndex, grid)
@staticmethod
def addTorsionTorsion(force, atom1, atom2, atom3, atom4, atom5, chiralIndex, gridIndex):
"""Add a torsion-torsion interaction to the force."""
force.addTorsionTorsion(atom1, atom2, atom3, atom4, atom5, chiralIndex, gridIndex)
@staticmethod
def addTorsionTorsionInteractions(force, data, types1, types2, types3, types4, types5, gridIndex, sys):
"""Add torsion-torsion interactions based on TINKER subroutine bitors()"""
for angle in data.angles:
# search for bitorsions; based on TINKER subroutine bitors()
ib = angle[0]
ic = angle[1]
id = angle[2]
for bondIndex in data.atomBonds[ib]:
bondedAtom1 = data.bonds[bondIndex].atom1
bondedAtom2 = data.bonds[bondIndex].atom2
if (bondedAtom1 != ib):
ia = bondedAtom1
else:
ia = bondedAtom2
if (ia != ic and ia != id):
for bondIndex2 in data.atomBonds[id]:
bondedAtom1 = data.bonds[bondIndex2].atom1
bondedAtom2 = data.bonds[bondIndex2].atom2
if (bondedAtom1 != id):
ie = bondedAtom1
else:
ie = bondedAtom2
if (ie != ic and ie != ib and ie != ia):
# found candidate set of atoms
# check if types match in order or reverse order
type1 = data.atomType[data.atoms[ia]]
type2 = data.atomType[data.atoms[ib]]
type3 = data.atomType[data.atoms[ic]]
type4 = data.atomType[data.atoms[id]]
type5 = data.atomType[data.atoms[ie]]
for i in range(len(types1)):
types1_i = types1[i]
types2_i = types2[i]
types3_i = types3[i]
types4_i = types4[i]
types5_i = types5[i]
# match in order
if (type1 in types1_i and type2 in types2_i and type3 in types3_i and
type4 in types4_i and type5 in types5_i):
chiralAtomIndex = AmoebaTorsionTorsionForceBuilder._getChiralAtomIndex(data, ib, ic, id)
force.addTorsionTorsion(ia, ib, ic, id, ie, chiralAtomIndex, gridIndex[i])
# match in reverse order
elif (type5 in types1_i and type4 in types2_i and type3 in types3_i and
type2 in types4_i and type1 in types5_i):
chiralAtomIndex = AmoebaTorsionTorsionForceBuilder._getChiralAtomIndex(data, ib, ic, id)
force.addTorsionTorsion(ie, id, ic, ib, ia, chiralAtomIndex, gridIndex[i])
@staticmethod
def createTorsionTorsionInteractions(force, angles, atoms, tortorParams):
""" """
Create torsion-torsion interactions based on the angles and tortor parameters. Create torsion-torsion interactions for both ForceField and TinkerFiles usage.
Parameters Parameters
---------- ----------
force : mm.AmoebaTorsionTorsionForce force : mm.AmoebaTorsionTorsionForce
The OpenMM AmoebaTorsionTorsionForce object The OpenMM AmoebaTorsionTorsionForce to add interactions to.
angles : list torsions : List[Tuple[int, ...]]
List of angles in the system List of torsion tuples containing atom indices.
atoms : list atomClasses : List[str]
List of TinkerAtom objects List of atom class strings indexed by atom index.
tortorParams : list masses : List[float]
List of torsion-torsion parameter sets List of atom masses indexed by atom index.
""" """
# Determine bonded atoms from the torsions
atomBonds = [[] for _ in range(len(atomClasses))]
for torsion in torsions:
for i in range(len(torsion) - 1):
atom1, atom2 = torsion[i], torsion[i+1]
if atom2 not in atomBonds[atom1]:
atomBonds[atom1].append(atom2)
if atom1 not in atomBonds[atom2]:
atomBonds[atom2].append(atom1)
bonds = [(atom1, atom2) for atom1, bonded in enumerate(atomBonds) for atom2 in bonded if atom1 < atom2]
# Get angles from torsions
angles_set = set()
for torsion in torsions:
for i in range(len(torsion) - 2):
angles_set.add((torsion[i], torsion[i+1], torsion[i+2]))
angles = list(angles_set)
# Create torsion-torsion interactions
addedInteractions = set()
for angle in angles: for angle in angles:
# angle = (atom1, atom2, atom3) where atom2 is the central atom ib, ic, id = angle
ib = angle[0] # first atom of angle for ia in atomBonds[ib]:
ic = angle[1] # central atom of angle if ia != ic:
id = angle[2] # last atom of angle for ie in atomBonds[id]:
# Find atoms bonded to ib (excluding ic and id)
for ia in atoms[ib].bonds:
if ia != ic and ia != id:
# Find atoms bonded to id (excluding ic and ib and ia)
for ie in atoms[id].bonds:
if ie != ic and ie != ib and ie != ia: if ie != ic and ie != ib and ie != ia:
# We have a potential torsion-torsion pattern: ia-ib-ic-id-ie interaction = tuple(sorted([(ia, ib, ic, id, ie), (ie, id, ic, ib, ia)]))
# Check if atom types match any tortor parameters if interaction in addedInteractions:
for gridIndex, (tortorInfo, gridData) in enumerate(tortorParams): continue
class1_param = tortorInfo[0] atomTypes = (atomClasses[ia], atomClasses[ib], atomClasses[ic], atomClasses[id], atomClasses[ie])
class2_param = tortorInfo[1] for torsionTorsionType, gridIdx in self.torsionTorsionParams:
class3_param = tortorInfo[2] if atomTypes == torsionTorsionType:
class4_param = tortorInfo[3] chiralIndex = AmoebaTorsionTorsionForceBuilder._getChiralIndex(ib, ic, id, atomClasses, bonds, masses)
class5_param = tortorInfo[4] force.addTorsionTorsion(ia, ib, ic, id, ie, chiralIndex, gridIdx)
addedInteractions.add(interaction)
type1 = atoms[ia].atomClass break
type2 = atoms[ib].atomClass elif atomTypes == tuple(reversed(torsionTorsionType)):
type3 = atoms[ic].atomClass chiralIndex = AmoebaTorsionTorsionForceBuilder._getChiralIndex(ib, ic, id, atomClasses, bonds, masses)
type4 = atoms[id].atomClass force.addTorsionTorsion(ie, id, ic, ib, ia, chiralIndex, gridIdx)
type5 = atoms[ie].atomClass addedInteractions.add(interaction)
break
# Check forward direction
if (type1 == class1_param and type2 == class2_param and
type3 == class3_param and type4 == class4_param and type5 == class5_param):
# Find chiral atom index
chiralIndex = AmoebaTorsionTorsionForceBuilder._getChiralAtomIndex(atoms, ib, ic, id)
AmoebaTorsionTorsionForceBuilder.addTorsionTorsion(force, ia, ib, ic, id, ie, chiralIndex, gridIndex)
# Check reverse direction
elif (type5 == class1_param and type4 == class2_param and
type3 == class3_param and type2 == class4_param and type1 == class5_param):
# Find chiral atom index
chiralIndex = AmoebaTorsionTorsionForceBuilder._getChiralAtomIndex(atoms, ib, ic, id)
AmoebaTorsionTorsionForceBuilder.addTorsionTorsion(force, ie, id, ic, ib, ia, chiralIndex, gridIndex)
@staticmethod @staticmethod
def _getChiralAtomIndex(data, atomB, atomC, atomD): def _getChiralIndex(atomB: int, atomC: int, atomD: int, atomClasses: List[str], bonds: List[Tuple[int, int]], masses: List[float]) -> int:
""" """
Get the chiral atom index based on the TINKER algorithm. Get chiral atom index.
Parameters Parameters
---------- ----------
data : ForceFieldData or list
ForceFieldData object (for ForceField usage) or list of TinkerAtom objects (for TinkerFiles usage)
atomB : int atomB : int
Index of atom B Index of first atom in the central angle.
atomC : int atomC : int
Index of atom C (central atom) Index of central atom that should have 4 bonds for chiral determination.
atomD : int atomD : int
Index of atom D Index of third atom in the central angle.
atomClasses : List[str]
List of atom class strings indexed by atom index.
bonds : List[Tuple[int, int]]
List of bonds defined by tuples of (atom1, atom2).
masses : List[float]
List of particle masses indexed by atom index.
Returns Returns
------- -------
int int
Index of the chiral atom, or -1 if not found Index of the chiral atom (atomE or atomF) based on TINKER algorithm,
""" or -1 if atomC doesn't have exactly 4 bonds or other conditions aren't met.
chiralAtomIndex = -1 """
if len(bonds[atomC]) == 4:
# Check if we're dealing with ForceField data (has atomBonds attribute) or TinkerAtom list otherAtoms = [atom for atom in bonds(atomC) if atom != atomB and atom != atomD]
if hasattr(data, 'atomBonds'): if len(otherAtoms) == 2:
# ForceField usage - data is ForceFieldData object atomE, atomF = otherAtoms
atoms = data.atoms typeE, typeF = atomClasses[atomE], atomClasses[atomF]
atomBonds = data.atomBonds if typeE > typeF:
bonds = data.bonds return atomE
atomType = data.atomType elif typeF > typeE:
return atomF
# Get atoms bonded to atomC massE, massF = masses[atomE], masses[atomF]
bondedAtoms = []
for bondIndex in atomBonds[atomC]:
bond = bonds[bondIndex]
if bond.atom1 == atomC:
bondedAtoms.append(bond.atom2)
else:
bondedAtoms.append(bond.atom1)
# If atomC has four bonds, find the two bonds that do not include atomB and atomD
if len(bondedAtoms) == 4:
atomE = -1
atomF = -1
for bondedAtom in bondedAtoms:
if bondedAtom != atomB and bondedAtom != atomD:
if atomE == -1:
atomE = bondedAtom
else:
atomF = bondedAtom
# Raise error if atoms E or F not found
if atomE == -1 or atomF == -1:
raise ValueError(f"getChiralAtomIndex: error getting bonded partners of atomC={atomC}")
# Check for different type between atoms E & F
typeE = atomType[atoms[atomE]]
typeF = atomType[atoms[atomF]]
if typeE and typeF:
# Compare atom types as strings/identifiers
if str(typeE) > str(typeF):
chiralAtomIndex = atomE
elif str(typeF) > str(typeE):
chiralAtomIndex = atomF
# If types are same, check masses
if chiralAtomIndex == -1:
massE = atoms[atomE].element.mass.value_in_unit(amu) if atoms[atomE].element else 0.0
massF = atoms[atomF].element.mass.value_in_unit(amu) if atoms[atomF].element else 0.0
if massE > massF:
chiralAtomIndex = atomE
elif massF > massE:
chiralAtomIndex = atomF
else:
# TinkerFiles usage - data is list of TinkerAtom objects
atoms = data
# If atomC has four bonds, find the two bonds that do not include atomB and atomD
# Set chiralAtomIndex to one of these, if they are not the same atom (type/mass)
atomC_bonds = atoms[atomC].bonds
if len(atomC_bonds) == 4:
atomE = -1
atomF = -1
for bondedAtom in atomC_bonds:
if bondedAtom != atomB and bondedAtom != atomD:
if atomE == -1:
atomE = bondedAtom
else:
atomF = bondedAtom
# Raise error if atoms E or F not found
if atomE == -1 or atomF == -1:
raise ValueError(f"getChiralAtomIndex: error getting bonded partners of atomC={atomC}")
# Check for different type/mass between atoms E & F
typeE = atoms[atomE].atomClass
typeF = atoms[atomF].atomClass
if typeE and typeF:
if int(typeE) > int(typeF):
chiralAtomIndex = atomE
elif int(typeF) > int(typeE):
chiralAtomIndex = atomF
# If types are same, check masses
if chiralAtomIndex == -1:
massE = atoms[atomE].mass if atoms[atomE].mass else 0.0
massF = atoms[atomF].mass if atoms[atomF].mass else 0.0
if massE > massF: if massE > massF:
chiralAtomIndex = atomE return atomE
elif massF > massE: elif massF > massE:
chiralAtomIndex = atomF return atomF
return chiralAtomIndex return -1
class AmoebaWcaDispersionForceBuilder(BaseAmoebaForceBuilder): class AmoebaWcaDispersionForceBuilder(BaseAmoebaForceBuilder):
...@@ -1933,17 +2197,15 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -1933,17 +2197,15 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
savedMultipoleParams = multipoleParams savedMultipoleParams = multipoleParams
hit = True hit = True
# assign multipole parameters via 1-2 and 1-3 connected atoms # Assign multipole parameters via 1-2 and 1-3 connected atoms
for multipoleParams in multipoleList: for multipoleParams in multipoleList:
if hit: if hit:
break break
kz, kx, ky = multipoleParams.kIndices[1:4] kz, kx, ky = multipoleParams.kIndices[1:4]
# assign multipole parameters # Assign multipole parameters
# (1) get bonded partners # (1) get bonded partners
# (2) match parameter types # (2) match parameter types
bondedAtom12Indices = self.bonded12ParticleSets[atomIndex] bondedAtom12Indices = self.bonded12ParticleSets[atomIndex]
bondedAtom13Indices = self.bonded13ParticleSets[atomIndex] bondedAtom13Indices = self.bonded13ParticleSets[atomIndex]
zaxis = -1 zaxis = -1
...@@ -1963,8 +2225,7 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -1963,8 +2225,7 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
zaxis = bondedAtomZIndex zaxis = bondedAtomZIndex
xaxis = bondedAtomXIndex xaxis = bondedAtomXIndex
# select xaxis w/ smallest index # Select xaxis w/ smallest index
for bondedAtomXIndex2 in bondedAtom13Indices: for bondedAtomXIndex2 in bondedAtom13Indices:
bondedAtomX1Type = atomTypes[bondedAtomXIndex2] bondedAtomX1Type = atomTypes[bondedAtomXIndex2]
if bondedAtomX1Type == kx and bondedAtomXIndex2 != bondedAtomZIndex and bondedAtomZIndex in self.bonded12ParticleSets[bondedAtomXIndex2] and bondedAtomXIndex2 < xaxis: if bondedAtomX1Type == kx and bondedAtomXIndex2 != bondedAtomZIndex and bondedAtomZIndex in self.bonded12ParticleSets[bondedAtomXIndex2] and bondedAtomXIndex2 < xaxis:
...@@ -1984,8 +2245,7 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -1984,8 +2245,7 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
savedMultipoleParams = multipoleParams savedMultipoleParams = multipoleParams
hit = True hit = True
# assign multipole parameters via only a z-defining atom # Assign multipole parameters via only a z-defining atom
for multipoleParams in multipoleList: for multipoleParams in multipoleList:
if hit: if hit:
break break
...@@ -2002,8 +2262,7 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2002,8 +2262,7 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
savedMultipoleParams = multipoleParams savedMultipoleParams = multipoleParams
hit = True hit = True
# assign multipole parameters via no connected atoms # Assign multipole parameters via no connected atoms
for multipoleParams in multipoleList: for multipoleParams in multipoleList:
if hit: if hit:
break break
...@@ -2036,21 +2295,18 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2036,21 +2295,18 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
raise ValueError('No multipole type for atom %d (%s %s %d)' % (atomIndex, atom.name, atom.residue.name, atom.residue.index)) raise ValueError('No multipole type for atom %d (%s %s %d)' % (atomIndex, atom.name, atom.residue.name, atom.residue.index))
# Set up the polarization groups. # Set up the polarization groups.
self.setPolarGroups(force, atomTypes) self.setPolarGroups(force, atomTypes)
def buildBondedParticleSets(self, numAtoms, bonds): def buildBondedParticleSets(self, numAtoms, bonds):
"""Identify sets of particles that are separated by various numbers of bonds.""" """Identify sets of particles that are separated by various numbers of bonds."""
# 1-2 # 1-2
bonded12ParticleSets = [set() for _ in range(numAtoms)] bonded12ParticleSets = [set() for _ in range(numAtoms)]
for atom1, atom2 in bonds: for atom1, atom2 in bonds:
bonded12ParticleSets[atom1].add(atom2) bonded12ParticleSets[atom1].add(atom2)
bonded12ParticleSets[atom2].add(atom1) bonded12ParticleSets[atom2].add(atom1)
# 1-3 # 1-3
bonded13ParticleSets = [] bonded13ParticleSets = []
for i in range(numAtoms): for i in range(numAtoms):
bonded13Set = set() bonded13Set = set()
...@@ -2059,7 +2315,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2059,7 +2315,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
bonded13Set = bonded13Set.union(bonded12ParticleSets[j]) bonded13Set = bonded13Set.union(bonded12ParticleSets[j])
# remove 1-2 and self from set # remove 1-2 and self from set
bonded13Set = bonded13Set - bonded12ParticleSet bonded13Set = bonded13Set - bonded12ParticleSet
selfSet = set() selfSet = set()
selfSet.add(i) selfSet.add(i)
...@@ -2068,7 +2323,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2068,7 +2323,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
bonded13ParticleSets.append(bonded13Set) bonded13ParticleSets.append(bonded13Set)
# 1-4 # 1-4
bonded14ParticleSets = [] bonded14ParticleSets = []
for i in range(numAtoms): for i in range(numAtoms):
bonded14Set = set() bonded14Set = set()
...@@ -2077,7 +2331,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2077,7 +2331,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
bonded14Set = bonded14Set.union(bonded12ParticleSets[j]) bonded14Set = bonded14Set.union(bonded12ParticleSets[j])
# remove 1-3, 1-2 and self from set # remove 1-3, 1-2 and self from set
bonded14Set = bonded14Set - bonded12ParticleSets[i] bonded14Set = bonded14Set - bonded12ParticleSets[i]
bonded14Set = bonded14Set - bonded13ParticleSet bonded14Set = bonded14Set - bonded13ParticleSet
selfSet = set() selfSet = set()
...@@ -2087,7 +2340,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2087,7 +2340,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
bonded14ParticleSets.append(bonded14Set) bonded14ParticleSets.append(bonded14Set)
# 1-5 # 1-5
bonded15ParticleSets = [] bonded15ParticleSets = []
for i in range(numAtoms): for i in range(numAtoms):
bonded15Set = set() bonded15Set = set()
...@@ -2096,7 +2348,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2096,7 +2348,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
bonded15Set = bonded15Set.union(bonded12ParticleSets[j]) bonded15Set = bonded15Set.union(bonded12ParticleSets[j])
# remove 1-4, 1-3, 1-2 and self from set # remove 1-4, 1-3, 1-2 and self from set
bonded15Set = bonded15Set - bonded12ParticleSets[i] bonded15Set = bonded15Set - bonded12ParticleSets[i]
bonded15Set = bonded15Set - bonded13ParticleSets[i] bonded15Set = bonded15Set - bonded13ParticleSets[i]
bonded15Set = bonded15Set - bonded14ParticleSet bonded15Set = bonded15Set - bonded14ParticleSet
...@@ -2118,7 +2369,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2118,7 +2369,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
for atomIndex in range(len(atomTypes)): for atomIndex in range(len(atomTypes)):
# assign multipole parameters via only 1-2 connected atoms # assign multipole parameters via only 1-2 connected atoms
groupTypes = self.polarizationParams[atomTypes[atomIndex]].groupAtomTypes groupTypes = self.polarizationParams[atomTypes[atomIndex]].groupAtomTypes
bondedAtomIndices = self.bonded12ParticleSets[atomIndex] bondedAtomIndices = self.bonded12ParticleSets[atomIndex]
polarizationGroupForAtom[atomIndex].add(atomIndex) polarizationGroupForAtom[atomIndex].add(atomIndex)
...@@ -2129,7 +2379,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2129,7 +2379,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
polarizationGroupForAtom[bondedAtomIndex].add(atomIndex) polarizationGroupForAtom[bondedAtomIndex].add(atomIndex)
# pgrp11 # pgrp11
for atomIndex in range(len(atomTypes)): for atomIndex in range(len(atomTypes)):
if len(groupSetsForAtom[atomIndex]) > 0: if len(groupSetsForAtom[atomIndex]) > 0:
continue continue
...@@ -2157,7 +2406,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2157,7 +2406,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent11, groupSetsForAtom[atomIndex][0]) force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent11, groupSetsForAtom[atomIndex][0])
# pgrp12 # pgrp12
for atomIndex in range(len(atomTypes)): for atomIndex in range(len(atomTypes)):
if len(groupSetsForAtom[atomIndex]) > 1: if len(groupSetsForAtom[atomIndex]) > 1:
continue continue
...@@ -2175,7 +2423,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2175,7 +2423,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent12, groupSetsForAtom[atomIndex][1]) force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent12, groupSetsForAtom[atomIndex][1])
# pgrp13 # pgrp13
for atomIndex in range(len(atomTypes)): for atomIndex in range(len(atomTypes)):
if len(groupSetsForAtom[atomIndex]) > 2: if len(groupSetsForAtom[atomIndex]) > 2:
continue continue
...@@ -2195,7 +2442,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder): ...@@ -2195,7 +2442,6 @@ class AmoebaMultipoleForceBuilder(BaseAmoebaForceBuilder):
force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent13, groupSetsForAtom[atomIndex][2]) force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent13, groupSetsForAtom[atomIndex][2])
# pgrp14 # pgrp14
for atomIndex in range(len(atomTypes)): for atomIndex in range(len(atomTypes)):
if len(groupSetsForAtom[atomIndex]) > 3: if len(groupSetsForAtom[atomIndex]) > 3:
continue continue
...@@ -2376,9 +2622,7 @@ class AmoebaVdwForceBuilder(BaseAmoebaForceBuilder): ...@@ -2376,9 +2622,7 @@ class AmoebaVdwForceBuilder(BaseAmoebaForceBuilder):
bonds : List[Tuple[int, int]] bonds : List[Tuple[int, int]]
List of bond indices as tuples of (atom1, atom2). List of bond indices as tuples of (atom1, atom2).
""" """
# Define types # Define types
sigmaScale = 1 sigmaScale = 1
if self.radiustype == 'SIGMA': if self.radiustype == 'SIGMA':
sigmaScale = 1.122462048309372 sigmaScale = 1.122462048309372
...@@ -2390,14 +2634,12 @@ class AmoebaVdwForceBuilder(BaseAmoebaForceBuilder): ...@@ -2390,14 +2634,12 @@ class AmoebaVdwForceBuilder(BaseAmoebaForceBuilder):
classToTypeMap[className] = force.addParticleType(sigma*sigmaScale, epsilon) classToTypeMap[className] = force.addParticleType(sigma*sigmaScale, epsilon)
# Record what other atoms each atom is bonded to. # Record what other atoms each atom is bonded to.
bondedParticleSets = [set() for _ in range(len(atoms))] bondedParticleSets = [set() for _ in range(len(atoms))]
for atom1, atom2 in bonds: for atom1, atom2 in bonds:
bondedParticleSets[atom1].add(atom2) bondedParticleSets[atom1].add(atom2)
bondedParticleSets[atom2].add(atom1) bondedParticleSets[atom2].add(atom1)
# add particles to force # Add particles to force
for i, atom in enumerate(atoms): for i, atom in enumerate(atoms):
className = atomClasses[i] className = atomClasses[i]
if className not in self.classParams: if className not in self.classParams:
...@@ -2413,28 +2655,22 @@ class AmoebaVdwForceBuilder(BaseAmoebaForceBuilder): ...@@ -2413,28 +2655,22 @@ class AmoebaVdwForceBuilder(BaseAmoebaForceBuilder):
force.addParticle(ivIndex, classToTypeMap[className], reduction) force.addParticle(ivIndex, classToTypeMap[className], reduction)
# Add pairs # Add pairs
for c1, c2, sigma, epsilon in self.pairParams: for c1, c2, sigma, epsilon in self.pairParams:
force.addTypePair(classToTypeMap[c1], classToTypeMap[c2], sigma, epsilon) force.addTypePair(classToTypeMap[c1], classToTypeMap[c2], sigma, epsilon)
# set combining rules # set combining rules
# set particle exclusions: self, 1-2 and 1-3 bonds # set particle exclusions: self, 1-2 and 1-3 bonds
# (1) collect in bondedParticleSets[i], 1-2 indices for all bonded partners of particle i # (1) collect in bondedParticleSets[i], 1-2 indices for all bonded partners of particle i
# (2) add 1-2,1-3 and self to exclusion set # (2) add 1-2,1-3 and self to exclusion set
for i in range(len(atoms)): for i in range(len(atoms)):
# 1-2 partners # 1-2 partners
exclusionSet = bondedParticleSets[i].copy() exclusionSet = bondedParticleSets[i].copy()
# 1-3 partners # 1-3 partners
if self.vdw13Scale == 0.0: if self.vdw13Scale == 0.0:
for bondedParticle in bondedParticleSets[i]: for bondedParticle in bondedParticleSets[i]:
exclusionSet = exclusionSet.union(bondedParticleSets[bondedParticle]) exclusionSet = exclusionSet.union(bondedParticleSets[bondedParticle])
# self # self
exclusionSet.add(i) exclusionSet.add(i)
force.setParticleExclusions(i, tuple(exclusionSet)) force.setParticleExclusions(i, tuple(exclusionSet))
...@@ -523,22 +523,23 @@ class TinkerFiles: ...@@ -523,22 +523,23 @@ class TinkerFiles:
# List of bonds as (atom1_index, atom2_index) used by various force builders # List of bonds as (atom1_index, atom2_index) used by various force builders
bonds = [(at1.index, at2.index) for at1, at2 in self.topology.bonds()] bonds = [(at1.index, at2.index) for at1, at2 in self.topology.bonds()]
# List of atoms bonded to each atom used by various force builders
bondedToAtom = [[] for _ in range(self.topology.getNumAtoms())]
for (at1, at2) in bonds:
bondedToAtom[at1].append(at2)
bondedToAtom[at2].append(at1)
# Add AmoebaBondForce # Add AmoebaBondForce
if "bond" in self._forces: if "bond" in self._forces:
bondParams = {(at1, at2): {"k": float(k), "r0": float(r0)} for at1, at2, k, r0 in self._forces["bond"]} bondParams = {(at1, at2): {"k": float(k)*100.0*4.184, "r0": float(r0)*0.1} for at1, at2, k, r0 in self._forces["bond"]}
bondForceBuilder = AmoebaBondForceBuilder(float(self._scalars["bond-cubic"])*10, float(self._scalars["bond-quartic"])*100) bondForceBuilder = AmoebaBondForceBuilder(float(self._scalars["bond-cubic"])*10, float(self._scalars["bond-quartic"])*100)
for (class1, class2), params in bondParams.items(): for (class1, class2), params in bondParams.items():
bondForceBuilder.registerParams((class1, class2), params["r0"]*0.1, params["k"]*100.0*4.184) bondForceBuilder.registerParams((class1, class2), params["r0"], params["k"])
bondForce = bondForceBuilder.getForce(sys) bondForce = bondForceBuilder.getForce(sys)
bondForceBuilder.addBonds(bondForce, atomClasses, bonds) bondForceBuilder.addBonds(bondForce, atomClasses, bonds)
if "angle" in self._forces or "opbend" in self._forces or "anglep" in self._forces: if "angle" in self._forces or "opbend" in self._forces or "anglep" in self._forces:
# Find all unique angles # Find all unique angles
bondedToAtom = [[] for _ in range(self.topology.getNumAtoms())]
for (at1, at2) in bonds:
bondedToAtom[at1].append(at2)
bondedToAtom[at2].append(at1)
uniqueAngles = set() uniqueAngles = set()
for atom in range(len(bondedToAtom)): for atom in range(len(bondedToAtom)):
neighbors = bondedToAtom[atom] neighbors = bondedToAtom[atom]
...@@ -548,63 +549,18 @@ class TinkerFiles: ...@@ -548,63 +549,18 @@ class TinkerFiles:
uniqueAngles.add(angle) uniqueAngles.add(angle)
angles = sorted(list(uniqueAngles)) angles = sorted(list(uniqueAngles))
# Need to initialize here as used to create out-of-plane angles
if "opbend" in self._forces: if "opbend" in self._forces:
opbendParams = {(at1, at2, at3, at4): {"k": float(k)} for at1, at2, at3, at4, k in self._forces["opbend"]} opbendParams = {(at1, at2, at3 if at3 != '0' else '', at4 if at4 != '0' else ''): {"k": float(k)} for at1, at2, at3, at4, k in self._forces["opbend"]}
else: else:
opbendParams = {} opbendParams = {}
# Classify angles into in-plane, out-of-plane and generic. # Classify angles into in-plane, out-of-plane, and generic
# If middle atom has covalency of 3 and the types of the middle atom and the partner atom (atom bonded to opbendTypes = list(opbendParams.keys())
# middle atom, but not in angle) match types1 and types2, then three out-of-plane bend angles are generated. inPlaneAngles, outOfPlaneAngles, genericAngles = AmoebaOutOfPlaneBendForceBuilder.classifyAngles(
# Three in-plane angle are also generated. angles, atomClasses, bondedToAtom, opbendTypes
# If the conditions are not satisfied, the angle is marked as 'generic' angle (not a in-plane angle). )
genericAngles = []
inPlaneAngles = []
outOfPlaneAngles = []
skipAtoms = {}
for angle in angles:
middleAtom = angle[1]
middleClass = atomClasses[middleAtom]
middleValence = len(self.atoms[middleAtom].bonds)
if middleValence == 3 and middleAtom not in skipAtoms:
partners = []
for partner in self.atoms[middleAtom].bonds:
partnerClass = atomClasses[partner]
for opBendClass in opbendParams:
if middleClass == opBendClass[1] and partnerClass == opBendClass[0]:
partners.append(partner)
break
if len(partners) == 3:
outOfPlaneAngles.append([partners[0], middleAtom, partners[1], partners[2]])
outOfPlaneAngles.append([partners[2], middleAtom, partners[0], partners[1]])
outOfPlaneAngles.append([partners[1], middleAtom, partners[2], partners[0]])
skipAtoms[middleAtom] = set(partners[:3])
angleList = list(angle[:3])
for atomIndex in partners:
if atomIndex not in angleList:
angleList.append(atomIndex)
inPlaneAngles.append(angleList)
else:
angleList = list(angle[:3])
for atomIndex in partners:
if atomIndex not in angleList:
angleList.append(atomIndex)
genericAngles.append(angleList)
elif middleValence == 3 and middleAtom in skipAtoms:
angleList = list(angle[:3])
for atomIndex in skipAtoms[middleAtom]:
if atomIndex not in angleList:
angleList.append(atomIndex)
inPlaneAngles.append(angleList)
else:
genericAngles.append(list(angle))
# Sanity checks for angle classification assert len(inPlaneAngles) + len(genericAngles) == len(angles), (
assert len(inPlaneAngles) + len(genericAngles)== len(angles), (
f"Angle classification mismatch:\n" f"Angle classification mismatch:\n"
f" in-plane: {len(inPlaneAngles)}\n" f" in-plane: {len(inPlaneAngles)}\n"
f" generic: {len(genericAngles)}\n" f" generic: {len(genericAngles)}\n"
...@@ -630,138 +586,52 @@ class TinkerFiles: ...@@ -630,138 +586,52 @@ class TinkerFiles:
angleParams = {(at1, at2, at3): {"k": float(k), "theta0": [float(theta) for theta in theta]} for at1, at2, at3, k, *theta in self._forces["angle"]} angleParams = {(at1, at2, at3): {"k": float(k), "theta0": [float(theta) for theta in theta]} for at1, at2, at3, k, *theta in self._forces["angle"]}
if genericAngles: if genericAngles:
angleForceBuilder = AmoebaAngleForceBuilder(self._scalars["angle-cubic"], self._scalars["angle-quartic"], self._scalars["angle-pentic"], self._scalars["angle-sextic"]) angleForceBuilder = AmoebaAngleForceBuilder(self._scalars["angle-cubic"], self._scalars["angle-quartic"], self._scalars["angle-pentic"], self._scalars["angle-sextic"])
processedParams = set()
processedAngles = []
for angle in genericAngles: for angle in genericAngles:
class1 = atomClasses[angle[0]] angleKey = (atomClasses[angle[0]], atomClasses[angle[1]], atomClasses[angle[2]])
class2 = atomClasses[angle[1]]
class3 = atomClasses[angle[2]]
params = angleParams.get((class1, class2, class3)) or angleParams.get((class3, class2, class1))
if params:
if len(params["theta0"]) > 1:
# Get k-index by counting number of non-angle hydrogens on the central atom
partners = [at for at in bondedToAtom[angle[1]] if at not in angle]
nHyd = sum(1 for i in partners if self.atoms[i].atomicNumber == 1)
if nHyd < len(params["theta0"]):
theta0 = params["theta0"][nHyd]
else:
raise ValueError(f"Angle parameters out of range for atom classes {class1}-{class2}-{class3}")
else:
theta0 = params["theta0"][0]
angleTuple = (angle[0], angle[1], angle[2]) angleTuple = (angle[0], angle[1], angle[2])
idealAngles[angleTuple] = theta0 # Store ideal angle for stretch-bend params = angleParams.get(angleKey) or angleParams.get(angleKey[::-1])
theta0 = angleForceBuilder.getIdealAngle(angleTuple, params["theta0"], self.atoms, bondedToAtom)
idealAngles[angleTuple] = theta0*math.pi/180 # Store ideal angle for stretch-bend
angleForceBuilder.registerParams(angleTuple, (theta0, params["k"]*4.184*(math.pi/180)**2)) angleForceBuilder.registerParams(angleTuple, (theta0, params["k"]*4.184*(math.pi/180)**2))
processedAngles.append(angleTuple)
if processedAngles:
angleForce = angleForceBuilder.getForce(sys) angleForce = angleForceBuilder.getForce(sys)
angleForceBuilder.addAngles(angleForce, processedAngles) angleForceBuilder.addAngles(angleForce, genericAngles)
# Add AmoebaInPlaneAngleForce # Add AmoebaInPlaneAngleForce
if "anglep" in self._forces: if "anglep" in self._forces:
inPlaneAngleParams = {(at1, at2, at3): {"k": float(k), "theta0": [float(theta) for theta in theta]} for at1, at2, at3, k, *theta in self._forces["anglep"]} inPlaneAngleParams = {(at1, at2, at3): {"k": float(k), "theta0": [float(theta) for theta in theta]} for at1, at2, at3, k, *theta in self._forces["anglep"]}
else: else:
inPlaneAngleParams = {} inPlaneAngleParams = {}
inPlaneAngleParams.update(angleParams) # Poltype e.g. does not use the "anglep" keyword, but uses "angle" keyword for in-plane angles inPlaneAngleParams.update(angleParams) # Poltype e.g. does not use the "anglep" keyword, but uses "angle" keyword for in-plane angles
if inPlaneAngles: if inPlaneAngles:
inPlaneAngleForceBuilder = AmoebaInPlaneAngleForceBuilder(self._scalars["angle-cubic"], self._scalars["angle-quartic"], self._scalars["angle-pentic"], self._scalars["angle-sextic"]) inPlaneAngleForceBuilder = AmoebaInPlaneAngleForceBuilder(self._scalars["angle-cubic"], self._scalars["angle-quartic"], self._scalars["angle-pentic"], self._scalars["angle-sextic"])
processedParams = set()
for angle in inPlaneAngles: for angle in inPlaneAngles:
class1 = atomClasses[angle[0]] angleKey = (atomClasses[angle[0]], atomClasses[angle[1]], atomClasses[angle[2]], atomClasses[angle[3]])
class2 = atomClasses[angle[1]] params = inPlaneAngleParams.get(angleKey[:3]) or inPlaneAngleParams.get(angleKey[:3][::-1])
class3 = atomClasses[angle[2]] theta0 = angleForceBuilder.getIdealAngle(tuple(angle), params["theta0"], self.atoms, bondedToAtom)
params = inPlaneAngleParams.get((class1, class2, class3)) or inPlaneAngleParams.get((class3, class2, class1)) idealAngles[tuple(angle)] = theta0*math.pi/180 # Store ideal angle for stretch-bend
if params: inPlaneAngleForceBuilder.registerParams(angleKey[:3], (theta0, params["k"]*4.184*(math.pi/180)**2))
class4 = atomClasses[angle[3]]
paramKey = (class1, class2, class3, class4)
if len(params["theta0"]) > 1:
# Get k-index by counting number of non-angle hydrogens on the central atom
partners = [at for at in bondedToAtom[angle[1]] if at not in angle[:]]
nHyd = sum(1 for i in partners if self.atoms[i].atomicNumber == 1)
if nHyd < len(params["theta0"]):
theta0 = params["theta0"][nHyd]
else:
raise ValueError(f"Angle parameters out of range for atom classes {class1}-{class2}-{class3}")
else:
theta0 = params["theta0"][0]
idealAngles[tuple(angle)] = theta0 # Store ideal angle for stretch-bend
if paramKey not in processedParams:
inPlaneAngleForceBuilder.registerParams(paramKey, (theta0, params["k"]*4.184*(math.pi/180)**2))
processedParams.add(paramKey)
if processedParams:
inPlaneAngleForce = inPlaneAngleForceBuilder.getForce(sys) inPlaneAngleForce = inPlaneAngleForceBuilder.getForce(sys)
inPlaneAngleForceBuilder.addInPlaneAngles(inPlaneAngleForce, atomClasses, inPlaneAngles) inPlaneAngleForceBuilder.addInPlaneAngles(inPlaneAngleForce, atomClasses, inPlaneAngles)
# Add AmoebaStretchBendForce # Add AmoebaStretchBendForce
if "strbnd" in self._forces: if "strbnd" in self._forces:
stretchBendParams = {(at1, at2, at3): {"k1": float(k1), "k2": float(k2)} for at1, at2, at3, k1, k2 in self._forces["strbnd"]} stretchBendParams = {(at1, at2, at3): {"k1": float(k1)*41.84*math.pi/180, "k2": float(k2)*41.84*math.pi/180} for at1, at2, at3, k1, k2 in self._forces["strbnd"]}
stretchBendForceBuilder = AmoebaStretchBendForceBuilder() stretchBendForceBuilder = AmoebaStretchBendForceBuilder()
processedAngles = [] processedAngles = stretchBendForceBuilder.registerAllStretchBendParams(atomClasses, genericAngles + inPlaneAngles, stretchBendParams, bondParams, idealAngles)
for angle in genericAngles + inPlaneAngles:
class1 = atomClasses[angle[0]]
class2 = atomClasses[angle[1]]
class3 = atomClasses[angle[2]]
params = stretchBendParams.get((class1, class2, class3))
if params is not None:
swap = False
else:
params = stretchBendParams.get((class3, class2, class1))
swap = params is not None
if params:
idealAngle = idealAngles.get(tuple(angle), None)
if idealAngle is None:
continue
if not swap:
# 1-2-3
bondAB = bondParams.get((class1, class2)) or bondParams.get((class2, class1))
bondCB = bondParams.get((class2, class3)) or bondParams.get((class3, class2))
if bondAB and bondCB:
bondAB, bondCB = bondAB['r0'], bondCB['r0']
k1, k2 = params["k1"], params["k2"]
else:
# 3-2-1
bondAB = bondParams.get((class3, class2)) or bondParams.get((class2, class3))
bondCB = bondParams.get((class2, class1)) or bondParams.get((class1, class2))
if bondAB and bondCB:
bondAB, bondCB = bondCB['r0'], bondAB['r0']
k1, k2 = params["k2"], params["k1"]
if bondAB and bondCB:
# Because for the same class triplet, depending on the number of hydrogens on the central atom,
# the ideal angle can be different, we need to register parameters for each angle.
paramKey = (angle[0], angle[1], angle[2])
stretchBendForceBuilder.registerParams(paramKey, (bondAB*0.1, bondCB*0.1, idealAngle*math.pi/180, k1*41.84*math.pi/180, k2*41.84*math.pi/180))
processedAngles.append((angle[0], angle[1], angle[2]))
if processedAngles:
stretchBendForce = stretchBendForceBuilder.getForce(sys) stretchBendForce = stretchBendForceBuilder.getForce(sys)
stretchBendForceBuilder.addStretchBends(stretchBendForce, processedAngles) stretchBendForceBuilder.addStretchBends(stretchBendForce, processedAngles)
# Add AMOEBA Urey-Bradley force # Add AmoebaUreyBradleyForce
if "ureybrad" in self._forces: if "ureybrad" in self._forces:
ureyBradleyParams = {(at1, at2, at3): {"k": float(k), "d": float(d)} for at1, at2, at3, k, d in self._forces["ureybrad"]} ureyBradleyParams = {(at1, at2, at3): {"k": float(k), "d": float(d)} for at1, at2, at3, k, d in self._forces["ureybrad"]}
ureyBradleyForceBuilder = AmoebaUreyBradleyForceBuilder() ureyBradleyForceBuilder = AmoebaUreyBradleyForceBuilder()
for (class1, class2, class3), params in ureyBradleyParams.items(): for (class1, class2, class3), params in ureyBradleyParams.items():
ureyBradleyForceBuilder.registerParams((class1, class2, class3), (params["k"]*4.184*100.0, params["d"]*0.1)) ureyBradleyForceBuilder.registerParams((class1, class2, class3), (params["k"]*4.184*100.0, params["d"]*0.1), isClass=True)
processedAngles = []
for at1, at2, at3 in angles:
class1 = atomClasses[at1]
class2 = atomClasses[at2]
class3 = atomClasses[at3]
params = ureyBradleyParams.get((class1, class2, class3)) or ureyBradleyParams.get((class3, class2, class1))
if params:
processedAngles.append((at1, at2, at3))
if processedAngles:
ureyBradleyForce = ureyBradleyForceBuilder.getForce(sys) ureyBradleyForce = ureyBradleyForceBuilder.getForce(sys)
ureyBradleyForceBuilder.addUreyBradleys(ureyBradleyForce, atomClasses, processedAngles) ureyBradleyForceBuilder.addUreyBradleys(ureyBradleyForce, atomClasses, angles, isClass=True)
# Find all unique proper torsions in the system # Find all unique proper torsions
uniquePropers = set() uniquePropers = set()
for angle in angles: for angle in angles:
for atom in self.atoms[angle[0]].bonds: for atom in self.atoms[angle[0]].bonds:
...@@ -785,57 +655,22 @@ class TinkerFiles: ...@@ -785,57 +655,22 @@ class TinkerFiles:
"t2": [float(t21)*torsionScale, float(t22)*math.pi/180.0, int(t23)], "t2": [float(t21)*torsionScale, float(t22)*math.pi/180.0, int(t23)],
"t3": [float(t31)*torsionScale, float(t32)*math.pi/180.0, int(t33)]} "t3": [float(t31)*torsionScale, float(t32)*math.pi/180.0, int(t33)]}
for at1, at2, at3, at4, t11, t12, t13, t21, t22, t23, t31, t32, t33 in self._forces["torsion"]} for at1, at2, at3, at4, t11, t12, t13, t21, t22, t23, t31, t32, t33 in self._forces["torsion"]}
torsionForceBuilder = AmoebaTorsionForceBuilder() torsionForceBuilder = AmoebaTorsionForceBuilder()
for (class1, class2, class3, class4), params in torsionParams.items(): for (class1, class2, class3, class4), params in torsionParams.items():
torsionForceBuilder.registerParams((class1, class2, class3, class4), (params["t1"], params["t2"], params["t3"])) torsionForceBuilder.registerParams((class1, class2, class3, class4), (params["t1"], params["t2"], params["t3"]))
processedTorsions = []
for at1, at2, at3, at4 in propers:
class1 = atomClasses[at1]
class2 = atomClasses[at2]
class3 = atomClasses[at3]
class4 = atomClasses[at4]
params = torsionParams.get((class1, class2, class3, class4)) or torsionParams.get((class4, class3, class2, class1))
if params:
processedTorsions.append((at1, at2, at3, at4))
if processedTorsions:
torsionForce = torsionForceBuilder.getForce(sys) torsionForce = torsionForceBuilder.getForce(sys)
atomClassesDict = {} torsionForceBuilder.addTorsions(torsionForce, atomClasses, propers)
for atom_idx in range(len(self.atoms)):
atomClassesDict[atom_idx] = atomClasses[atom_idx]
torsionForceBuilder.addTorsions(torsionForce, atomClasses, processedTorsions)
# Add AmoebaPiTorsionForce # Add AmoebaPiTorsionForce
if "pitors" in self._forces: if "pitors" in self._forces:
piTorsionParams = {(at1, at2): {"k": float(k)} for at1, at2, k in self._forces["pitors"]}
piTorsionForceBuilder = AmoebaPiTorsionForceBuilder()
piTorsionScale = float(self._scalars["pitorsunit"])*4.184 piTorsionScale = float(self._scalars["pitorsunit"])*4.184
piTorsionParams = {(at1, at2): {"k": float(k)*piTorsionScale} for at1, at2, k in self._forces["pitors"]}
piTorsionForceBuilder = AmoebaPiTorsionForceBuilder()
for (class1, class2), params in piTorsionParams.items(): for (class1, class2), params in piTorsionParams.items():
piTorsionForceBuilder.registerParams((class1, class2), (params["k"]*piTorsionScale,)) piTorsionForceBuilder.registerParams((class1, class2), (params["k"],))
processedPiTorsions = piTorsionForceBuilder.getAllPiTorsions(atomClasses, bondedToAtom, bonds)
processedPiTorsions = []
for (at1, at2) in bonds:
valence1 = len(self.atoms[at1].bonds)
valence2 = len(self.atoms[at2].bonds)
if valence1 == 3 and valence2 == 3:
class1 = atomClasses[at1]
class2 = atomClasses[at2]
params = piTorsionParams.get((class1, class2)) or piTorsionParams.get((class2, class1))
if params:
piTorsionAtom3 = at1
piTorsionAtom4 = at2
# piTorsionAtom1, piTorsionAtom2 are the atoms bonded to atom1, excluding atom2
# piTorsionAtom5, piTorsionAtom6 are the atoms bonded to atom2, excluding atom1
piTorsionAtom1, piTorsionAtom2 = [bond for bond in self.atoms[at1].bonds if bond != piTorsionAtom4]
piTorsionAtom5, piTorsionAtom6 = [bond for bond in self.atoms[at2].bonds if bond != piTorsionAtom3]
processedPiTorsions.append((piTorsionAtom1, piTorsionAtom2, piTorsionAtom3, piTorsionAtom4, piTorsionAtom5, piTorsionAtom6))
if processedPiTorsions:
piTorsionForce = piTorsionForceBuilder.getForce(sys) piTorsionForce = piTorsionForceBuilder.getForce(sys)
piTorsionForceBuilder.addPiTorsions(piTorsionForce, atomClassesDict, processedPiTorsions) piTorsionForceBuilder.addPiTorsions(piTorsionForce, atomClasses, processedPiTorsions)
# Add AmoebaStretchTorsionForce # Add AmoebaStretchTorsionForce
if "strtors" in self._forces: if "strtors" in self._forces:
...@@ -844,18 +679,8 @@ class TinkerFiles: ...@@ -844,18 +679,8 @@ class TinkerFiles:
stretchTorsionForceBuilder = AmoebaStretchTorsionForceBuilder() stretchTorsionForceBuilder = AmoebaStretchTorsionForceBuilder()
for classes, params in stretchTorsionParams.items(): for classes, params in stretchTorsionParams.items():
stretchTorsionForceBuilder.registerParams(classes, params) stretchTorsionForceBuilder.registerParams(classes, params)
processedStretchTorsions = []
for at1, at2, at3, at4 in propers:
class1 = atomClasses[at1]
class2 = atomClasses[at2]
class3 = atomClasses[at3]
class4 = atomClasses[at4]
params = stretchTorsionParams.get((class1, class2, class3, class4)) or stretchTorsionParams.get((class4, class3, class2, class1))
if params:
processedStretchTorsions.append((at1, at2, at3, at4))
if processedStretchTorsions:
stretchTorsionForce = stretchTorsionForceBuilder.getForce(sys) stretchTorsionForce = stretchTorsionForceBuilder.getForce(sys)
stretchTorsionForceBuilder.addStretchTorsions(sys, stretchTorsionForce, atomClasses, processedStretchTorsions) stretchTorsionForceBuilder.addStretchTorsions(sys, stretchTorsionForce, atomClasses, propers)
# Add AmoebaAngleTorsionForce # Add AmoebaAngleTorsionForce
if "angtors" in self._forces: if "angtors" in self._forces:
...@@ -864,30 +689,22 @@ class TinkerFiles: ...@@ -864,30 +689,22 @@ class TinkerFiles:
angleTorsionForceBuilder = AmoebaAngleTorsionForceBuilder() angleTorsionForceBuilder = AmoebaAngleTorsionForceBuilder()
for classes, params in angleTorsionParams.items(): for classes, params in angleTorsionParams.items():
angleTorsionForceBuilder.registerParams(classes, params) angleTorsionForceBuilder.registerParams(classes, params)
processedAngleTorsions = []
for at1, at2, at3, at4 in propers:
class1 = atomClasses[at1]
class2 = atomClasses[at2]
class3 = atomClasses[at3]
class4 = atomClasses[at4]
params = angleTorsionParams.get((class1, class2, class3, class4)) or angleTorsionParams.get((class4, class3, class2, class1))
if params:
processedAngleTorsions.append((at1, at2, at3, at4))
if processedAngleTorsions:
angleTorsionForce = angleTorsionForceBuilder.getForce(sys) angleTorsionForce = angleTorsionForceBuilder.getForce(sys)
angleTorsionForceBuilder.addAngleTorsions(sys, angleTorsionForce, atomClasses, processedAngleTorsions) angleTorsionForceBuilder.addAngleTorsions(sys, angleTorsionForce, atomClasses, propers)
# Add AmoebaTorsionTorsionForce # Add AmoebaTorsionTorsionForce
if "tortors" in self._forces: if "tortors" in self._forces:
torsionTorsionForce = AmoebaTorsionTorsionForceBuilder() torsionTorsionForceBuilder = AmoebaTorsionTorsionForceBuilder()
for gridIndex, (tortorInfo, gridData) in enumerate(self._forces["tortors"]): for gridIndex, (tortorInfo, gridData) in enumerate(self._forces["tortors"]):
tortorType = (tortorInfo[0], tortorInfo[1], tortorInfo[2], tortorInfo[3], tortorInfo[4])
torsionTorsionForceBuilder.registerParams(tortorType, gridIndex)
nx = int(tortorInfo[5]) nx = int(tortorInfo[5])
ny = int(tortorInfo[6]) ny = int(tortorInfo[6])
grid = np.array(gridData, dtype=np.float64).reshape((nx, ny, -1)) grid = np.array(gridData, dtype=np.float64).reshape((nx, ny, -1))
grid[:, :, 2] *= 4.184 grid[:, :, 2] *= 4.184
torsionTorsionForce.registerGridData(gridIndex, grid) torsionTorsionForceBuilder.registerGridData(gridIndex, grid)
force = torsionTorsionForce.getForce(sys) torsionTorsionForce = torsionTorsionForceBuilder.getForce(sys)
AmoebaTorsionTorsionForceBuilder.createTorsionTorsionInteractions(force, angles, self.atoms, self._forces["tortors"]) torsionTorsionForceBuilder.addTorsionTorsionInteractions(torsionTorsionForce, propers, atomClasses, [atom.mass for atom in self.atoms])
# Add AmoebaVdwForce # Add AmoebaVdwForce
if "vdw" in self._forces: if "vdw" in self._forces:
...@@ -923,7 +740,6 @@ class TinkerFiles: ...@@ -923,7 +740,6 @@ class TinkerFiles:
# Add AmoebaGeneralizedKirkwoodForce # Add AmoebaGeneralizedKirkwoodForce
if implicitSolvent and "multipole" in self._forces: if implicitSolvent and "multipole" in self._forces:
gkForceBuilder = AmoebaGeneralizedKirkwoodForceBuilder(**self.GK_PARAMS) gkForceBuilder = AmoebaGeneralizedKirkwoodForceBuilder(**self.GK_PARAMS)
force = gkForceBuilder.getForce(sys, implicitSolvent=implicitSolvent)
multipoleForce = [f for f in sys.getForces() if isinstance(f, mm.AmoebaMultipoleForce)][0] multipoleForce = [f for f in sys.getForces() if isinstance(f, mm.AmoebaMultipoleForce)][0]
for atomIndex in range(0, multipoleForce.getNumMultipoles()): for atomIndex in range(0, multipoleForce.getNumMultipoles()):
multipoleParameters = multipoleForce.getMultipoleParameters(atomIndex) multipoleParameters = multipoleForce.getMultipoleParameters(atomIndex)
...@@ -939,15 +755,12 @@ class TinkerFiles: ...@@ -939,15 +755,12 @@ class TinkerFiles:
convert *= 1.122462048309372 convert *= 1.122462048309372
if self._scalars["radiussize"] == "DIAMETER": if self._scalars["radiussize"] == "DIAMETER":
convert *= 0.5 convert *= 0.5
# Register VdW parameters for each atom class
for vdw in self._forces["vdw"]: for vdw in self._forces["vdw"]:
atomClass = int(vdw[0]) atomClass = int(vdw[0])
sigma = float(vdw[1])*convert sigma = float(vdw[1])*convert
epsilon = float(vdw[2])*4.184 epsilon = float(vdw[2])*4.184
wcaDispersionForceBuilder.registerParams(atomClass, sigma, epsilon) wcaDispersionForceBuilder.registerParams(atomClass, sigma, epsilon)
wcaDispersionForce = wcaDispersionForceBuilder.getForce(sys) wcaDispersionForce = wcaDispersionForceBuilder.getForce(sys)
atomClasses = [int(atom.atomClass) for atom in self.atoms]
wcaDispersionForceBuilder.addParticles(wcaDispersionForce, atomClasses) wcaDispersionForceBuilder.addParticles(wcaDispersionForce, atomClasses)
# Set periodic boundary conditions # Set periodic boundary conditions
...@@ -973,7 +786,6 @@ class TinkerFiles: ...@@ -973,7 +786,6 @@ class TinkerFiles:
return sys return sys
# ------------------------------------------------------------------------------------------ # # ------------------------------------------------------------------------------------------ #
# TOPOLOGY FUNCTIONS # # TOPOLOGY FUNCTIONS #
# ------------------------------------------------------------------------------------------ # # ------------------------------------------------------------------------------------------ #
......
...@@ -1958,12 +1958,12 @@ class AmoebaTestForceField(unittest.TestCase): ...@@ -1958,12 +1958,12 @@ class AmoebaTestForceField(unittest.TestCase):
# Compare to values computed with Tinker. # Compare to values computed with Tinker.
self.assertAlmostEqual(290.2445, energies['AmoebaBond'], 4) self.assertAlmostEqual(290.2445, energies['AmoebaBondForce'], 4)
self.assertAlmostEqual(496.4300, energies['AmoebaAngle']+energies['AmoebaInPlaneAngle'], 4) self.assertAlmostEqual(496.4300, energies['AmoebaAngleForce']+energies['AmoebaInPlaneAngleForce'], 4)
self.assertAlmostEqual(51.2913, energies['AmoebaOutOfPlaneBend'], 4) self.assertAlmostEqual(51.2913, energies['AmoebaOutOfPlaneBendForce'], 4)
self.assertAlmostEqual(5.7695, energies['AmoebaStretchBend'], 4) self.assertAlmostEqual(5.7695, energies['AmoebaStretchBendForce'], 4)
self.assertAlmostEqual(75.6890, energies['PeriodicTorsionForce'], 4) self.assertAlmostEqual(75.6890, energies['PeriodicTorsionForce'], 4)
self.assertAlmostEqual(19.3364, energies['AmoebaPiTorsion'], 4) self.assertAlmostEqual(19.3364, energies['AmoebaPiTorsionForce'], 4)
self.assertAlmostEqual(-32.6689, energies['AmoebaTorsionTorsionForce'], 4) self.assertAlmostEqual(-32.6689, energies['AmoebaTorsionTorsionForce'], 4)
self.assertAlmostEqual(383.8705, energies['AmoebaVdwForce'], 4) self.assertAlmostEqual(383.8705, energies['AmoebaVdwForce'], 4)
self.assertAlmostEqual(-1323.5640-225.3660, energies['AmoebaMultipoleForce'], 2) self.assertAlmostEqual(-1323.5640-225.3660, energies['AmoebaMultipoleForce'], 2)
...@@ -1975,14 +1975,14 @@ class AmoebaTestForceField(unittest.TestCase): ...@@ -1975,14 +1975,14 @@ class AmoebaTestForceField(unittest.TestCase):
# Compare to values computed with Tinker. # Compare to values computed with Tinker.
self.assertAlmostEqual(749.6953, energies['AmoebaBond'], 4) self.assertAlmostEqual(749.6953, energies['AmoebaBondForce'], 4)
self.assertAlmostEqual(579.9971, energies['AmoebaAngle']+energies['AmoebaInPlaneAngle'], 4) self.assertAlmostEqual(579.9971, energies['AmoebaAngleForce']+energies['AmoebaInPlaneAngleForce'], 4)
self.assertAlmostEqual(10.6630, energies['AmoebaOutOfPlaneBend'], 4) self.assertAlmostEqual(10.6630, energies['AmoebaOutOfPlaneBendForce'], 4)
self.assertAlmostEqual(5.2225, energies['AmoebaStretchBend'], 4) self.assertAlmostEqual(5.2225, energies['AmoebaStretchBendForce'], 4)
self.assertAlmostEqual(166.7233, energies['PeriodicTorsionForce'], 4) self.assertAlmostEqual(166.7233, energies['PeriodicTorsionForce'], 4)
self.assertAlmostEqual(57.2066, energies['AmoebaPiTorsion'], 4) self.assertAlmostEqual(57.2066, energies['AmoebaPiTorsionForce'], 4)
self.assertAlmostEqual(-4.2538, energies['AmoebaStretchTorsion'], 4) self.assertAlmostEqual(-4.2538, energies['AmoebaStretchTorsionForce'], 4)
self.assertAlmostEqual(-5.0402, energies['AmoebaAngleTorsion'], 4) self.assertAlmostEqual(-5.0402, energies['AmoebaAngleTorsionForce'], 4)
self.assertAlmostEqual(187.1103, energies['AmoebaVdwForce'], 4) self.assertAlmostEqual(187.1103, energies['AmoebaVdwForce'], 4)
self.assertAlmostEqual(1635.1289-236.1484, energies['AmoebaMultipoleForce'], 3) self.assertAlmostEqual(1635.1289-236.1484, energies['AmoebaMultipoleForce'], 3)
self.assertAlmostEqual(3146.3046, sum(list(energies.values())), 3) self.assertAlmostEqual(3146.3046, sum(list(energies.values())), 3)
......
...@@ -14,8 +14,6 @@ class TestTinkerFiles(unittest.TestCase): ...@@ -14,8 +14,6 @@ class TestTinkerFiles(unittest.TestCase):
polarization="mutual", polarization="mutual",
mutualInducedTargetEpsilon=1e-5, mutualInducedTargetEpsilon=1e-5,
nonbondedMethod=NoCutoff, nonbondedMethod=NoCutoff,
constraints=None,
useDispersionCorrection=False,
) )
# Compute the energy with OpenMM. # Compute the energy with OpenMM.
...@@ -91,10 +89,10 @@ class TestTinkerFiles(unittest.TestCase): ...@@ -91,10 +89,10 @@ class TestTinkerFiles(unittest.TestCase):
energies, _, _ = self.computeAmoebaEnergies(xyzFile, keyFiles) energies, _, _ = self.computeAmoebaEnergies(xyzFile, keyFiles)
# Compare to values computed with Tinker. # Compare to values computed with Tinker.
self.assertEnergyEqual(1104.0455, energies["AmoebaBond"]) self.assertEnergyEqual(1104.0455, energies["AmoebaBondForce"])
self.assertEnergyEqual(602.7082, energies["AmoebaAngle"] + energies["AmoebaInPlaneAngle"]) self.assertEnergyEqual(602.7082, energies["AmoebaAngleForce"] + energies["AmoebaInPlaneAngleForce"])
self.assertEnergyEqual(2.0572, energies["AmoebaOutOfPlaneBend"], 1e-4) self.assertEnergyEqual(2.0572, energies["AmoebaOutOfPlaneBendForce"], 1e-4)
self.assertEnergyEqual(-0.1361, energies["AmoebaStretchBend"], 1e-3) self.assertEnergyEqual(-0.1361, energies["AmoebaStretchBendForce"], 1e-3)
self.assertEnergyEqual(-0.8625, energies["PeriodicTorsionForce"], 1e-4) self.assertEnergyEqual(-0.8625, energies["PeriodicTorsionForce"], 1e-4)
self.assertEnergyEqual(-33.8595, energies["HarmonicBondForce"]) self.assertEnergyEqual(-33.8595, energies["HarmonicBondForce"])
self.assertEnergyEqual(5908.1343, energies["AmoebaVdwForce"]) self.assertEnergyEqual(5908.1343, energies["AmoebaVdwForce"])
...@@ -156,12 +154,12 @@ class TestTinkerFiles(unittest.TestCase): ...@@ -156,12 +154,12 @@ class TestTinkerFiles(unittest.TestCase):
'LYS', 'ARG'], f'Unexpected residues: {residues}' 'LYS', 'ARG'], f'Unexpected residues: {residues}'
# Compare to values computed with Tinker. # Compare to values computed with Tinker.
self.assertEnergyEqual(19.6519, energies["AmoebaBond"]) self.assertEnergyEqual(19.6519, energies["AmoebaBondForce"])
self.assertEnergyEqual(58.2509, energies["AmoebaAngle"] + energies["AmoebaInPlaneAngle"]) self.assertEnergyEqual(58.2509, energies["AmoebaAngleForce"] + energies["AmoebaInPlaneAngleForce"])
self.assertEnergyEqual(1.9697, energies["AmoebaOutOfPlaneBend"], 1e-4) self.assertEnergyEqual(1.9697, energies["AmoebaOutOfPlaneBendForce"], 1e-4)
self.assertEnergyEqual(-0.4384, energies["AmoebaStretchBend"], 1e-3) self.assertEnergyEqual(-0.4384, energies["AmoebaStretchBendForce"], 1e-3)
self.assertEnergyEqual(-2.3514, energies["PeriodicTorsionForce"], 1e-4) self.assertEnergyEqual(-2.3514, energies["PeriodicTorsionForce"], 1e-4)
self.assertEnergyEqual(1.2115, energies["AmoebaPiTorsion"], 1e-4) self.assertEnergyEqual(1.2115, energies["AmoebaPiTorsionForce"], 1e-4)
self.assertEnergyEqual(-3.2958, energies["AmoebaTorsionTorsionForce"]) self.assertEnergyEqual(-3.2958, energies["AmoebaTorsionTorsionForce"])
self.assertEnergyEqual(1509.1915, energies["AmoebaVdwForce"]) self.assertEnergyEqual(1509.1915, energies["AmoebaVdwForce"])
self.assertEnergyEqual(-488.0403 - 110.9042, energies["AmoebaMultipoleForce"], 1e-3) self.assertEnergyEqual(-488.0403 - 110.9042, energies["AmoebaMultipoleForce"], 1e-3)
...@@ -224,14 +222,14 @@ class TestTinkerFiles(unittest.TestCase): ...@@ -224,14 +222,14 @@ class TestTinkerFiles(unittest.TestCase):
'G', 'C', 'G', 'U', 'U', 'A', 'A', 'G', 'U', 'C', 'G', 'C', 'A'], f'Unexpected residues: {residues}' 'G', 'C', 'G', 'U', 'U', 'A', 'A', 'G', 'U', 'C', 'G', 'C', 'A'], f'Unexpected residues: {residues}'
# Compare to values computed with Tinker. # Compare to values computed with Tinker.
self.assertEnergyEqual(749.6953, energies["AmoebaBond"]) self.assertEnergyEqual(749.6953, energies["AmoebaBondForce"])
self.assertEnergyEqual(579.9971, energies["AmoebaAngle"] + energies["AmoebaInPlaneAngle"]) self.assertEnergyEqual(579.9971, energies["AmoebaAngleForce"] + energies["AmoebaInPlaneAngleForce"])
self.assertEnergyEqual(10.6630, energies["AmoebaOutOfPlaneBend"]) self.assertEnergyEqual(10.6630, energies["AmoebaOutOfPlaneBendForce"])
self.assertEnergyEqual(5.2225, energies["AmoebaStretchBend"]) self.assertEnergyEqual(5.2225, energies["AmoebaStretchBendForce"])
self.assertEnergyEqual(166.7233, energies["PeriodicTorsionForce"]) self.assertEnergyEqual(166.7233, energies["PeriodicTorsionForce"])
self.assertEnergyEqual(57.2066, energies["AmoebaPiTorsion"]) self.assertEnergyEqual(57.2066, energies["AmoebaPiTorsionForce"])
self.assertEnergyEqual(-4.2538, energies["AmoebaStretchTorsion"]) self.assertEnergyEqual(-4.2538, energies["AmoebaStretchTorsionForce"])
self.assertEnergyEqual(-0.0880, energies["AmoebaAngleTorsion"], 1e-4) self.assertEnergyEqual(-0.0880, energies["AmoebaAngleTorsionForce"], 1e-4)
self.assertEnergyEqual(187.1103, energies["AmoebaVdwForce"]) self.assertEnergyEqual(187.1103, energies["AmoebaVdwForce"])
self.assertEnergyEqual(1635.1289 - 236.1484, energies["AmoebaMultipoleForce"]) self.assertEnergyEqual(1635.1289 - 236.1484, energies["AmoebaMultipoleForce"])
self.assertEnergyEqual(3151.2568, sum(list(energies.values()))) self.assertEnergyEqual(3151.2568, sum(list(energies.values())))
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment