Repository: openmm/openmm — pull request #4219 (cpp)
Title: Don't add extra bond between water hydrogens

Fixes #4212.

Amber files list an extra "bond" between the hydrogens of a water molecule, which was incorrectly included in the Topology built from a prmtop file.  A Topology should only include actual chemical bonds.

Resolved issues:
title: TIP3P Residue Matching Issue w/ AMBER system
body: I'm using OpenMM to run a system I prepared using LEaP.

My workflow looks something like this:
-Use LEaP to prepare AMBER style PRMTOP/INPCRD files for a protein in explicit solvent with ff14SB/TIP3P
-Use OpenMM to parse the AMBER style files and create a forcefield object/perform residue to template matching
-This (functions in ```forcefield.py``` and the ```System``` object) is used by a custom simulation package to implement a forcefield and integration routine to do MD simulations

The issue I'm running into is happening when parsing the AMBER files into an OpenMM style system and doing residue to template matching. For this purpose, I'm using the ```amber14/protein.ff14SB.xml``` and ```amber14/tip3p.xml``` files located in the OpenMM directory. From previous experience, I understand that TIP3P in the AMBER convention includes an H-H bond to represent the rigid nature of the potential. When I load the files into OpenMM and check the residues:
```
prmtop = app.AmberPrmtopFile('../RAMP1_ion.prmtop')
inpcrd = app.AmberInpcrdFile('../RAMP1_ion.inpcrd')
residues = list(prmtop.topology.residues())
print(list(residues[5000].atoms()))
print(list(residues[5000].bonds()))
```
I see this behavior, which results in the expected O, H1, and H2 atoms as well as the expected 3 bonds.
```
[<Atom 15966 (O) of chain 0 residue 5000 (HOH)>,
<Atom 15967 (H1) of chain 0 residue 5000 (HOH)>,
<Atom 15968 (H2) of chain 0 residue 5000 (HOH)>]

[Bond(<Atom 15967 (H1) of chain 0 residue 5000 (HOH)>, <Atom 15966 (O) of chain 0 residue 5000 (HOH)>),
Bond(<Atom 15968 (H2) of chain 0 residue 5000 (HOH)>, <Atom 15966 (O) of chain 0 residue 5000 (HOH)>),
Bond(<Atom 15968 (H2) of chain 0 residue 5000 (HOH)>, <Atom 15967 (H1) of chain 0 residue 5000 (HOH)>)]
```

When looking at the ```amber14/tip3p.xml``` file, I notice that the only bonds defined are the two O-H bonds.

```
<Residue name="HOH">
   <Atom name="O" type="tip3p-O" charge="-0.834"/>
   <Atom name="H1" type="tip3p-H" charge="0.417"/>
   <Atom name="H2" type="tip3p-H" charge="0.417"/>
   <Bond atomName1="O" atomName2="H1"/>
   <Bond atomName1="O" atomName2="H2"/>
</Residue>
```
When the simulation package or OpenMM tries to perform the template matching using the functions in ```forcefield.py```, this results in an error:
```
#OpenMM example that produces the matching error
prmtop = AmberPrmtopFile('../RAMP1_ion.prmtop')
inpcrd = AmberInpcrdFile('../RAMP1_ion.inpcrd')
forcefield = ForceField('amber14/protein.ff14SB.xml', 'amber14/tip3p.xml')
system = forcefield.createSystem(prmtop.topology, nonbondedMethod=PME, nonbondedCutoff=8*angstrom, constraints=None)
```
```
ValueError: No template found for residue 122 (HOH).  The set of atoms matches HOH, but the bonds are different.
```
I've used ParmED / the ```amberprmtopfile.py``` tool included in OpenMM that completely bypasses ```forcefield.py``` to setup AMBER systems for OpenMM before but the tool I'm using would require significant rewriting to avoid this. I'm trying to figure out if there's anything I can do to deal with this extra bond either in system setup, parsing into OpenMM objects, or an alternative XML file I can use to resolve this issue.

## IMPORTANT INSTRUCTIONS

1. If you edit a python file, you MUST run 'make PythonInstall' inside the build directory
2. If you edit a c++ file, you MUST run 'make -j4 && make install" inside the build directory

Hints:

Fix patch:
diff --git a/wrappers/python/openmm/app/amberprmtopfile.py b/wrappers/python/openmm/app/amberprmtopfile.py
index d01a590d9a..250d4438a1 100644
--- a/wrappers/python/openmm/app/amberprmtopfile.py
+++ b/wrappers/python/openmm/app/amberprmtopfile.py
@@ -160,7 +160,13 @@ def __init__(self, file, periodicBoxVectors=None, unitCellDimensions=None):
 
         atoms = list(top.atoms())
         for bond in prmtop.getBondsWithH():
-            top.addBond(atoms[bond[0]], atoms[bond[1]])
+            a1 = atoms[bond[0]]
+            a2 = atoms[bond[1]]
+            if a1.residue.name == 'HOH' and a1.element == elem.hydrogen and a2.element == elem.hydrogen:
+                # Don't add the "bond" Amber lists between the two hydrogens of a water molecule.
+                pass
+            else:
+                top.addBond(a1, a2)
         for bond in prmtop.getBondsNoH():
             top.addBond(atoms[bond[0]], atoms[bond[1]])
 


Test patch:
diff --git a/wrappers/python/tests/TestAmberPrmtopFile.py b/wrappers/python/tests/TestAmberPrmtopFile.py
index aa190d4b64..f25a1e6cfa 100644
--- a/wrappers/python/tests/TestAmberPrmtopFile.py
+++ b/wrappers/python/tests/TestAmberPrmtopFile.py
@@ -78,12 +78,14 @@ def test_RigidWaterAndConstraints(self):
         """Test all eight options for the constraints and rigidWater parameters."""
 
         topology = prmtop1.topology
-        for constraints_value in [None, HBonds, AllBonds, HAngles]:
-            for rigidWater_value in [True, False]:
-                system = prmtop1.createSystem(constraints=constraints_value,
-                                              rigidWater=rigidWater_value)
-                validateConstraints(self, topology, system,
-                                    constraints_value, rigidWater_value)
+        for constraints in [None, HBonds, AllBonds, HAngles]:
+            for rigidWater in [True, False]:
+                system = prmtop1.createSystem(constraints=constraints, rigidWater=rigidWater)
+                if constraints != None:
+                    # Amber adds an extra "bond" between water hydrogens, so any constraint
+                    # method except None is equivalent to rigidWater=True.
+                    rigidWater = True
+                validateConstraints(self, topology, system, constraints, rigidWater)
 
     def test_ImplicitSolvent(self):
         """Test the four types of implicit solvents using the implicitSolvent
@@ -460,5 +462,16 @@ def testEPConstraints(self):
             # If a constraint was added to a massless particle, this will throw an exception.
             context = Context(system, integrator, Platform.getPlatformByName('Reference'))
 
+    def testWaterBonds(self):
+        """Test that water molecules have the right set of bonds"""
+        top = prmtop1.topology
+        for residue in top.residues():
+            if residue.name == 'HOH':
+                bonds = list(residue.bonds())
+                self.assertEqual(2, len(bonds))
+                for a1, a2 in bonds:
+                    self.assertTrue(a1.element == elem.oxygen or a2.element == elem.oxygen)
+                    self.assertTrue(a1.element == elem.hydrogen or a2.element == elem.hydrogen)
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/wrappers/python/tests/TestForceField.py b/wrappers/python/tests/TestForceField.py
index 767c7793d6..d9073b963a 100644
--- a/wrappers/python/tests/TestForceField.py
+++ b/wrappers/python/tests/TestForceField.py
@@ -439,7 +439,6 @@ def test_residueMatcher(self):
    <Atom name="H2" type="tip3p-H" charge="0.417"/>
    <Bond from="0" to="1"/>
    <Bond from="0" to="2"/>
-   <Bond from="1" to="2"/>
   </Residue>
   <Residue name="HOH2">
    <Atom name="O" type="tip3p-O" charge="0.834"/>
@@ -447,7 +446,6 @@ def test_residueMatcher(self):
    <Atom name="H2" type="tip3p-H" charge="-0.417"/>
    <Bond from="0" to="1"/>
    <Bond from="0" to="2"/>
-   <Bond from="1" to="2"/>
   </Residue>
  </Residues>
  <NonbondedForce coulomb14scale="0.833333" lj14scale="0.5">