Files
openmm/wrappers/python/tests/TestTinkerFiles.py
João Morado eaf56f96bc Update AMOEBA XML, .prm files, and parsing of Tinker files (#5086)
* Correct AmoebaAngleTorsion in test_Amoeba18Nucleic of TestForceField

* Update processTinkerForceField to handle latest .prm Tinker files

* Update amoeba2018 XML files

* Update amoeba2013 XML files

* Update amoeba2009 XML files

* Adapt addTorTor to new format in the .prm files

* Fix TorsionTorsion

* Also update the total energy in test_Amoeba18Nucleic

* Update amoebabio18.prm

* Fix nucleic acid test energies

* Correct AmoebaAngleTorsionForce params

* Add new addTorTor to TinkerFiles

* Revert unit fix

* Change to .pdb file which Tinker likes

* Update test_Amoeba18BPTI

* Remove trailing zeros from XML files

* Leave trailing zeros only on 2018 ff

* New element names in 2018

* More digits for surfaceAreaFactor

* More digits for surfaceAreaFactor

* More digits

* Remove debugging print

* Add support to 2009 and 2013 AMOEBA ffs to processTinkerForceField.py

* Add FF specific residues XML files

* Delete old residuesFinal.xml

* Update AMOEBA XML ffs

* Update FFs

* Fix some formatting issues

* Fix "." in scientific notation

* Remove old assertions
2025-10-14 12:55:21 -07:00

309 lines
15 KiB
Python

import unittest
from openmm import *
from openmm.app import *
from openmm.unit import *
class TestTinkerFiles(unittest.TestCase):
"""Test the TinkerFiles class for reading Tinker files and creating OpenMM systems."""
def computeAmoebaEnergies(self, xyzFile, keyFiles):
tinker = TinkerFiles(xyzFile, keyFiles)
system = tinker.createSystem(
polarization="mutual",
mutualInducedTargetEpsilon=1e-5,
nonbondedMethod=NoCutoff,
)
# Compute the energy with OpenMM.
for i, f in enumerate(system.getForces()):
f.setForceGroup(i)
integrator = VerletIntegrator(0.001)
context = Context(system, integrator, Platform.getPlatform("Reference"))
context.setPositions(tinker.getPositions())
energies = {}
for i, f in enumerate(system.getForces()):
state = context.getState(getEnergy=True, groups={i})
energies[f.getName()] = state.getPotentialEnergy().value_in_unit(
kilocalories_per_mole
)
return energies, tinker, system
def assertEnergyEqual(self, expected, found, rtol=1e-5):
"""Assert the values match to a specified relative precision."""
if abs(expected-found) > rtol*abs(expected):
raise AssertionError(f'{expected} != {found} to a relative tolerance of {rtol}')
def test_Amoeba18PoltypePhenolWater(self):
"""
Test that TinkerFiles generates a system that gives the same energies as Tinker for phenol (poltype) in water (amoebabio18).
Notes
-----
$ analyze systems/phenol_water.xyz systems/test.prm E
(test.prm is $ cat systems/phenol.prm systems/amoebabio18.prm > systems/test.prm)
Intermolecular Energy : -12759.7770 Kcal/mole
Total Potential Energy : -11084.9187 Kcal/mole
Energy Component Breakdown : Kcal/mole Interactions
Bond Stretching 1104.0455 3007
Angle Bending 602.7082 1516
Stretch-Bend -0.1361 19
Urey-Bradley -33.8595 1497
Out-of-Plane Bend 2.0572 18
Torsional Angle -0.8625 26
Van der Waals 5908.1343 10136233
Atomic Multipoles -13227.0088 10136233
Polarization -5439.9969 10136233
$ analyze systems/phenol_water.xyz systems/test.prm D
(test.prm is $ cat systems/phenol.prm systems/amoebabio18.prm > systems/test.prm)
Overall System Contents :
Number of Atoms 4504
Number of Molecules 1498
Total System Mass 27062.5680
Force Field Name : AMOEBA-BIO-2018
VDW Function BUFFERED-14-7
Size Descriptor R-MIN
Size Unit Type DIAMETER
Size Combining Rule CUBIC-MEAN
Well Depth Rule HHG
Electrostatics ATOMIC MULTIPOLE
Induction INDUCED DIPOLE
Polarization MUTUAL
Polarization THOLE DAMPING
"""
xyzFile = "systems/phenol_water.xyz"
keyFiles = ["systems/phenol.prm", "systems/amoebabio18.prm"]
energies, _, _ = self.computeAmoebaEnergies(xyzFile, keyFiles)
# Compare to values computed with Tinker.
self.assertEnergyEqual(1104.0455, energies["AmoebaBondForce"])
self.assertEnergyEqual(602.7082, energies["AmoebaAngleForce"] + energies["AmoebaInPlaneAngleForce"])
self.assertEnergyEqual(2.0572, energies["AmoebaOutOfPlaneBendForce"], 1e-4)
self.assertEnergyEqual(-0.1361, energies["AmoebaStretchBendForce"], 1e-3)
self.assertEnergyEqual(-0.8625, energies["PeriodicTorsionForce"], 1e-4)
self.assertEnergyEqual(-33.8595, energies["HarmonicBondForce"])
self.assertEnergyEqual(5908.1343, energies["AmoebaVdwForce"])
self.assertEnergyEqual(-13227.0088 - 5439.9969, energies["AmoebaMultipoleForce"])
self.assertEnergyEqual(-11084.9187, sum(list(energies.values())))
def test_Amoeba18Peptide(self):
"""
Test that TinkerFiles generates a system that gives the same energies as Tinker for a peptide.
Notes
-----
$ analyze systems/peptide.xyz systems/amoebabio18.prm E
Total Potential Energy : 985.2453 Kcal/mole
Energy Component Breakdown : Kcal/mole Interactions
Bond Stretching 19.6519 333
Angle Bending 58.2509 596
Stretch-Bend -0.4384 533
Out-of-Plane Bend 1.9697 225
Torsional Angle -2.3514 875
Pi-Orbital Torsion 1.2115 48
Torsion-Torsion -3.2958 1
Van der Waals 1509.1915 52699
Atomic Multipoles -488.0403 52699
Polarization -110.9042 52699
$ analyze systems/peptide.xyz systems/amoebabio18.prm D
Overall System Contents :
Number of Atoms 328
Number of Molecules 1
Total System Mass 2396.7620
Force Field Name : AMOEBA-BIO-2018
VDW Function BUFFERED-14-7
Size Descriptor R-MIN
Size Unit Type DIAMETER
Size Combining Rule CUBIC-MEAN
Well Depth Rule HHG
Electrostatics ATOMIC MULTIPOLE
Induction INDUCED DIPOLE
Polarization MUTUAL
Polarization THOLE DAMPING
"""
xyzFile = "systems/peptide.xyz"
keyFiles = ["systems/amoebabio18.prm"]
energies, tinker, _ = self.computeAmoebaEnergies(xyzFile, keyFiles)
# Assert residues are correct
residues = [residue.name for residue in tinker.topology.residues()]
assert residues == ['ALA', 'GLY', 'VAL', 'LEU', 'ILE', 'PRO', 'SER', 'THR', 'CYS',
'PHE', 'TYR', 'HIS', 'TRP', 'ASP', 'ASN', 'GLU', 'GLN', 'MET',
'LYS', 'ARG'], f'Unexpected residues: {residues}'
# Compare to values computed with Tinker.
self.assertEnergyEqual(19.6519, energies["AmoebaBondForce"])
self.assertEnergyEqual(58.2509, energies["AmoebaAngleForce"] + energies["AmoebaInPlaneAngleForce"])
self.assertEnergyEqual(1.9697, energies["AmoebaOutOfPlaneBendForce"], 1e-4)
self.assertEnergyEqual(-0.4384, energies["AmoebaStretchBendForce"], 1e-3)
self.assertEnergyEqual(-2.3514, energies["PeriodicTorsionForce"], 1e-4)
self.assertEnergyEqual(1.2115, energies["AmoebaPiTorsionForce"], 1e-4)
self.assertEnergyEqual(-3.2958, energies["AmoebaTorsionTorsionForce"])
self.assertEnergyEqual(1509.1915, energies["AmoebaVdwForce"])
self.assertEnergyEqual(-488.0403 - 110.9042, energies["AmoebaMultipoleForce"], 1e-3)
self.assertEnergyEqual(985.2453, sum(list(energies.values())), 1e-3)
def test_Amoeba18Nucleic(self):
"""
Test that TinkerFiles generates a system that gives the same energies as Tinker for DNA and RNA.
Notes
-----
$ analyze systems/nucleic.xyz systems/amoebabio18.prm E
Intermolecular Energy : 896.3435 Kcal/mole
Total Potential Energy : 3146.3045 Kcal/mole
Energy Component Breakdown : Kcal/mole Interactions
Bond Stretching 749.6953 827
Angle Bending 579.9971 1483
Stretch-Bend 5.2225 1253
Out-of-Plane Bend 10.6630 441
Torsional Angle 166.7233 2197
Pi-Orbital Torsion 57.2066 142
Stretch-Torsion -4.2538 68
Angle-Torsion -5.0402 112
Van der Waals 187.1103 291451
Atomic Multipoles 1635.1289 291451
Polarization -236.1484 291451
$ analyze systems/nucleic.xyz systems/amoebabio18.prm G
Overall System Contents :
Number of Atoms 767
Number of Molecules 2
Total System Mass 7500.6170
Force Field Name : AMOEBA-BIO-2018
VDW Function BUFFERED-14-7
Size Descriptor R-MIN
Size Unit Type DIAMETER
Size Combining Rule CUBIC-MEAN
Well Depth Rule HHG
Electrostatics ATOMIC MULTIPOLE
Induction INDUCED DIPOLE
Polarization MUTUAL
Polarization THOLE DAMPING
"""
xyzFile = "systems/nucleic.xyz"
keyFiles = ["systems/amoebabio18.prm"]
energies, tinker, _ = self.computeAmoebaEnergies(xyzFile, keyFiles)
# Assert residues are correct
residues = [residue.name for residue in tinker.topology.residues()]
assert residues == ['DA', 'DG', 'DC', 'DG', 'DT', 'DG', 'DG', 'DG', 'DA', 'DC', 'DC',
'G', 'C', 'G', 'U', 'U', 'A', 'A', 'G', 'U', 'C', 'G', 'C', 'A'], f'Unexpected residues: {residues}'
# Compare to values computed with Tinker.
self.assertEnergyEqual(749.6953, energies["AmoebaBondForce"])
self.assertEnergyEqual(579.9971, energies["AmoebaAngleForce"] + energies["AmoebaInPlaneAngleForce"])
self.assertEnergyEqual(10.6630, energies["AmoebaOutOfPlaneBendForce"])
self.assertEnergyEqual(5.2225, energies["AmoebaStretchBendForce"])
self.assertEnergyEqual(166.7233, energies["PeriodicTorsionForce"])
self.assertEnergyEqual(57.2066, energies["AmoebaPiTorsionForce"])
self.assertEnergyEqual(-4.2538, energies["AmoebaStretchTorsionForce"])
self.assertEnergyEqual(-5.0402, energies["AmoebaAngleTorsionForce"], 1e-3)
self.assertEnergyEqual(187.1103, energies["AmoebaVdwForce"])
self.assertEnergyEqual(1635.1289 - 236.1484, energies["AmoebaMultipoleForce"])
self.assertEnergyEqual(3146.3045, sum(list(energies.values())))
def test_Amoeba13ForcesImplicit(self):
"""Compute forces and compare them to ones generated with a previous version of OpenMM to ensure they haven't changed."""
# Define mapping between positions of the .xyz file created using
# pdbxyz alanine-dipeptide-implicit.pdb
# and the original .pdb file.
mapping = {1: 4, 2: 1, 3: 5, 4: 6, 5: 2, 6: 3, 7: 7, 8: 11,
9: 8, 10: 12, 11: 13, 12: 14, 13: 15, 14: 16, 15: 9,
16: 10, 17: 17, 18: 19, 19: 18, 20: 20, 21: 21, 22: 22
}
xyzFile = 'systems/alanine-dipeptide-implicit.xyz'
keyFiles = ['systems/amoebapro13.prm']
tinker = TinkerFiles(xyzFile, keyFiles)
system = tinker.createSystem(polarization='direct',
constraints=None,
implicitSolvent=True,
)
# Assert residues are correct
residues = [residue.name for residue in tinker.topology.residues()]
assert residues == ['ACE', 'ALA', 'NME'], f'Unexpected residues: {residues}'
# Compute the forces with OpenMM
integrator = VerletIntegrator(0.001)
context = Context(system, integrator, Platform.getPlatform('Reference'))
context.setPositions(tinker.getPositions())
state1 = context.getState(getForces=True)
with open('systems/alanine-dipeptide-amoeba-forces.xml') as input:
state2 = XmlSerializer.deserialize(input.read())
state1Forces = state1.getForces().value_in_unit(kilojoules_per_mole/nanometer)
state2Forces = state2.getForces().value_in_unit(kilojoules_per_mole/nanometer)
for i, j in mapping.items():
f1 = state1Forces[j-1]
f2 = state2Forces[i-1]
diff = norm(f1-f2)
self.assertTrue(diff < 0.1 or diff/norm(f1) < 1e-3, f1-f2)
def test_Topology(self):
"""Test that the Topology created from Tinker files is correct."""
xyzFile = "systems/ubiquitin.xyz"
keyFiles = ["systems/amoebabio18.prm"]
tinker = TinkerFiles(xyzFile, keyFiles)
topology = tinker.topology
# Test ubiquitin
assert topology.getNumAtoms() == 1406, f'Expected 1406 atoms for ubiquitin, found {topology.getNumAtoms()}'
residues = [residue.name for residue in topology.residues()]
assert residues == ["MET", "GLN", "ILE", "PHE", "VAL", "LYS", "THR", "LEU", "THR", "GLY", "LYS", "THR", "ILE", "THR", "LEU",
"GLU", "VAL", "GLU", "PRO", "SER", "ASP", "THR", "ILE", "GLU", "ASN", "VAL", "LYS", "ALA", "LYS", "ILE",
"GLN", "ASP", "LYS", "GLU", "GLY", "ILE", "PRO", "PRO", "ASP", "GLN", "GLN", "ARG", "LEU", "ILE", "PHE",
"ALA", "GLY", "LYS", "GLN", "LEU", "GLU", "ASP", "GLY", "ARG", "THR", "LEU", "SER", "ASP", "TYR", "ASN",
"ILE", "GLN", "LYS", "GLU", "SER", "THR", "LEU", "HIS", "LEU", "VAL", "LEU", "ARG", "LEU", "ARG", "GLY", "GLY"] + ["HOH"]*58, f'Unexpected residues: {residues}'
xyzFile = "systems/ubiquitin.xyz"
keyFiles = ["systems/amoebabio18.prm"]
tinker = TinkerFiles(xyzFile, keyFiles)
topology = tinker.topology
# Test bdna
xyzFile = "systems/bdna.xyz"
tinker = TinkerFiles(xyzFile, keyFiles)
topology = tinker.topology
assert topology.getNumAtoms() == 758, f'Expected 758 atoms for bdna, found {topology.getNumAtoms()}'
residues = [residue.name for residue in topology.residues()]
assert residues == ["DC", "DG", "DC", "DG", "DA", "DA", "DT", "DT", "DC", "DG", "DC", "DG"] * 2, f'Unexpected residues: {residues}'
assert topology.getNumChains() == 2, f'Expected 2 chains for bdna, found {topology.getNumChains()}'