Added option for box shape when building solvent boxes (openmm/openmm#3480)

Add truncated octahedral solvation option for Modeller.addSolvent()

It turns out that it's quite difficult to automate solvation within a truncated octahedron.

Could we add an option to `Modeller.addSolvent()` that would allow the user to select between standard box shapes?

cc : @glass-w, who can provide more information on use cases.

## 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

diff --git a/wrappers/python/openmm/app/modeller.py b/wrappers/python/openmm/app/modeller.py
index 0920053d26..47ee5d9787 100644
--- a/wrappers/python/openmm/app/modeller.py
+++ b/wrappers/python/openmm/app/modeller.py
@@ -6,7 +6,7 @@
 Biological Structures at Stanford, funded under the NIH Roadmap for
 Medical Research, grant U54 GM072970. See https://simtk.org.

-Portions copyright (c) 2012-2021 Stanford University and the Authors.
+Portions copyright (c) 2012-2022 Stanford University and the Authors.
 Authors: Peter Eastman
 Contributors:

@@ -48,7 +48,7 @@
 import sys
 import xml.etree.ElementTree as etree
 from copy import deepcopy
-from math import ceil, floor
+from math import ceil, floor, sqrt
 from collections import defaultdict

 class Modeller(object):
@@ -375,8 +375,8 @@ def _addIons(self, forcefield, numWaters, replaceableMols, ionCutoff=0.05*nanome
             self.topology = modeller.topology
             self.positions = modeller.positions

-    def addSolvent(self, forcefield, model='tip3p', boxSize=None, boxVectors=None, padding=None, numAdded=None, positiveIon='Na+', negativeIon='Cl-', ionicStrength=0*molar, neutralize=True):
-        """Add solvent (both water and ions) to the model to fill a rectangular box.
+    def addSolvent(self, forcefield, model='tip3p', boxSize=None, boxVectors=None, padding=None, numAdded=None, boxShape='cube', positiveIon='Na+', negativeIon='Cl-', ionicStrength=0*molar, neutralize=True):
+        """Add solvent (both water and ions) to the model to fill a periodic box.

         The algorithm works as follows:

@@ -390,12 +390,18 @@ def addSolvent(self, forcefield, model='tip3p', boxSize=None, boxVectors=None, p

         1. You can explicitly give the vectors defining the periodic box to use.
         2. Alternatively, for a rectangular box you can simply give the dimensions of the unit cell.
-        3. You can give a padding distance.  The largest dimension of the solute (along the x, y, or z axis) is determined, and a cubic
-           box of size (largest dimension)+2*padding is used.
-        4. You can specify the total number of molecules (both waters and ions) to add.  A cubic box is then created whose size is
+        3. You can give a padding distance.  A bounding sphere containing the solute is determined, and the box size is
+           set to (sphere radius)+(padding).  This guarantees no atom in the solute will come closer than the padding
+           distance to any atom of another periodic copy.
+        4. You can specify the total number of molecules (both waters and ions) to add.  A box is then created whose size is
            just large enough hold the specified amount of solvent.
         5. Finally, if none of the above options is specified, the existing Topology's box vectors are used.

+        When specifying either a padding distance or a number of molecules, you can specify a shape for the periodic box:
+        cubic, rhombic dodecahedron, or truncated octahedron.  Using a non-rectangular box allows the same distance
+        between periodic copies to be achieved with a smaller box.  The most compact option is a rhombic dodecahedron,
+        for which the box volume is 70.7% the volume of a cubic box with the same amount of padding.
+
         Parameters
         ----------
         forcefield : ForceField
@@ -410,6 +416,9 @@ def addSolvent(self, forcefield, model='tip3p', boxSize=None, boxVectors=None, p
             the padding distance to use
         numAdded : int=None
             the total number of molecules (waters and ions) to add
+        boxShape: str='cube'
+            the box shape to use.  Allowed values are 'cube', 'dodecahedron', and 'octahedron'.  If padding and numAdded
+            are both None, this is ignored.
         positiveIon : string='Na+'
             the type of positive ion to add.  Allowed values are 'Cs+', 'K+', 'Li+', 'Na+', and 'Rb+'
         negativeIon : string='Cl-'
@@ -449,7 +458,7 @@ def addSolvent(self, forcefield, model='tip3p', boxSize=None, boxVectors=None, p
         if numAdded is not None:
             # Select a padding distance which is guaranteed to give more than the specified number of molecules.

-            padding = 1.1*(numAdded/((len(pdbResidues)/pdbBoxSize[0]**3)*8))**(1.0/3.0)
+            padding = 2.2*(numAdded/((len(pdbResidues)/pdbBoxSize[0]**3)*8))**(1.0/3.0)
             if padding < 0.5:
                 padding = 0.5 # Ensure we have enough when adding very small numbers of molecules
         if boxVectors is not None:
@@ -466,12 +475,23 @@ def addSolvent(self, forcefield, model='tip3p', boxSize=None, boxVectors=None, p
             if is_quantity(padding):
                 padding = padding.value_in_unit(nanometer)
             if len(self.positions) == 0:
-                maxSize = 0
+                radius = 0
+            else:
+                positions = self.positions.value_in_unit(nanometer)
+                minRange = Vec3(*(min((pos[i] for pos in positions)) for i in range(3)))
+                maxRange = Vec3(*(max((pos[i] for pos in positions)) for i in range(3)))
+                center = 0.5*(minRange+maxRange)
+                radius = max(unit.norm(center-pos) for pos in positions)
+            width = 2*radius+padding
+            box = width*Vec3(1, 1, 1)
+            if boxShape == 'cube':
+                vectors = (Vec3(width, 0, 0), Vec3(0, width, 0), Vec3(0, 0, width))
+            elif boxShape == 'dodecahedron':
+                vectors = (Vec3(width, 0, 0), Vec3(0, width, 0), Vec3(0.5, 0.5, 0.5*sqrt(2))*width)
+            elif boxShape == 'octahedron':
+                vectors = (Vec3(width, 0, 0), Vec3(1/3, 2*sqrt(2)/3, 0)*width, Vec3(-1/3, sqrt(2)/3, sqrt(6)/3)*width)
             else:
-                maxSize = max(max((pos[i] for pos in self.positions))-min((pos[i] for pos in self.positions)) for i in range(3))
-                maxSize = maxSize.value_in_unit(nanometer)
-            box = (maxSize+2*padding)*Vec3(1, 1, 1)
-            vectors = (Vec3(maxSize+2*padding, 0, 0), Vec3(0, maxSize+2*padding, 0), Vec3(0, 0, maxSize+2*padding))
+                raise ValueError(f'Illegal box shape: {boxShape}')
         else:
             box = self.topology.getUnitCellDimensions().value_in_unit(nanometer)
             vectors = self.topology.getPeriodicBoxVectors().value_in_unit(nanometer)

diff --git a/wrappers/python/tests/TestModeller.py b/wrappers/python/tests/TestModeller.py
index ec65574f11..467ee8083a 100644
--- a/wrappers/python/tests/TestModeller.py
+++ b/wrappers/python/tests/TestModeller.py
@@ -1,7 +1,5 @@
 from collections import defaultdict
 import unittest
-import math
-import sys

 from validateModeller import *
 from openmm.app import *
@@ -333,7 +331,6 @@ def test_addSolventPeriodicBox(self):
         self.assertVecAlmostEqual(dim3[2]/nanometers, Vec3(0, 0, 5.5))

         # Second way of passing in the periodic box vectors: with the boxSize parameter to addSolvent()
-        topology_start = self.pdb.topology
         modeller = Modeller(topology_start, self.positions)
         modeller.deleteWater()
         modeller.addSolvent(self.forcefield, boxSize = Vec3(3.6, 4.6, 5.6)*nanometers)
@@ -345,7 +342,6 @@ def test_addSolventPeriodicBox(self):
         self.assertVecAlmostEqual(dim3[2]/nanometers, Vec3(0, 0, 5.6))

         # Third way of passing in the periodic box vectors: with the boxVectors parameter to addSolvent()
-        topology_start = self.pdb.topology
         modeller = Modeller(topology_start, self.positions)
         modeller.deleteWater()
         modeller.addSolvent(self.forcefield, boxVectors = (Vec3(3.4, 0, 0), Vec3(0.5, 4.4, 0), Vec3(-1.0, -1.5, 5.4))*nanometers)
@@ -357,25 +353,43 @@ def test_addSolventPeriodicBox(self):
         self.assertVecAlmostEqual(dim3[2]/nanometers, Vec3(-1.0, -1.5, 5.4))

         # Fourth way of passing in the periodic box vectors: pass a 'padding' value to addSolvent()
-        topology_start = self.pdb.topology
         modeller = Modeller(topology_start, self.positions)
         modeller.deleteWater()
         modeller.addSolvent(self.forcefield, padding = 1.0*nanometers)
         topology_after = modeller.getTopology()
         dim3 = topology_after.getPeriodicBoxVectors()

-        self.assertVecAlmostEqual(dim3[0]/nanometers, Vec3(2.8802, 0, 0))
-        self.assertVecAlmostEqual(dim3[1]/nanometers, Vec3(0, 2.8802, 0))
-        self.assertVecAlmostEqual(dim3[2]/nanometers, Vec3(0, 0, 2.8802))
+        self.assertVecAlmostEqual(dim3[0]/nanometers, Vec3(1.924363, 0, 0))
+        self.assertVecAlmostEqual(dim3[1]/nanometers, Vec3(0, 1.924363, 0))
+        self.assertVecAlmostEqual(dim3[2]/nanometers, Vec3(0, 0, 1.924363))

         # Fifth way: specify a number of molecules to add instead of a box size
-        topology_start = self.pdb.topology
         modeller = Modeller(topology_start, self.positions)
         modeller.deleteWater()
         numInitial = len(list(modeller.topology.residues()))
         modeller.addSolvent(self.forcefield, numAdded=1000)
         self.assertEqual(numInitial+1000, len(list(modeller.topology.residues())))

+    def test_addSolventBoxShape(self):
+        """Test the addSolvent() method; test the different box shapes."""
+        modeller = Modeller(self.pdb.topology, self.positions)
+        modeller.deleteWater()
+        modeller.addSolvent(self.forcefield, padding=1.0*nanometers, boxShape='cube')
+        cubeVectors = modeller.getTopology().getPeriodicBoxVectors()
+        modeller = Modeller(self.pdb.topology, self.positions)
+        modeller.deleteWater()
+        modeller.addSolvent(self.forcefield, padding=1.0*nanometers, boxShape='dodecahedron')
+        dodecVectors = modeller.getTopology().getPeriodicBoxVectors()
+        modeller = Modeller(self.pdb.topology, self.positions)
+        modeller.deleteWater()
+        modeller.addSolvent(self.forcefield, padding=1.0*nanometers, boxShape='octahedron')
+        octVectors = modeller.getTopology().getPeriodicBoxVectors()
+        cubeVolume = cubeVectors[0][0]*cubeVectors[1][1]*cubeVectors[2][2]/(nanometers**3)
+        dodecVolume = dodecVectors[0][0]*dodecVectors[1][1]*dodecVectors[2][2]/(nanometers**3)
+        octVolume = octVectors[0][0]*octVectors[1][1]*octVectors[2][2]/(nanometers**3)
+        self.assertAlmostEqual(0.707, dodecVolume/cubeVolume, places=3)
+        self.assertAlmostEqual(0.770, octVolume/cubeVolume, places=3)
+
     def test_addSolventNeutralSolvent(self):
         """ Test the addSolvent() method; test adding ions to neutral solvent. """


Ensure bond lengths are set before creating forces that depend on them (openmm/openmm#3493)

Switching the order of forcefield.xml files causes simulation to crash

Hi @peastman,

I have a rather strange bug happening for me. I have (finally) tracked it down to a switching of the strings when defining the forcefield. For example, if I specify:
`forcefield = ForceField("amoeba2018.xml", "formamide.xml")`
the simulation will run for a bit, then crash. However, If I switch the order of those and instead use:
`forcefield = ForceField("formamide.xml", "amoeba2018.xml")`
the simulation works as expected.

I am using OpenMM built from source at the current point on master.

Here are some test files (reference platform) that reproduce the error:

[fails_nve.py.txt](https://github.com/openmm/openmm/files/8150049/fails_nve.py.txt)
[formamide.xml.txt](https://github.com/openmm/openmm/files/8150050/formamide.xml.txt)
[system.pdb.txt](https://github.com/openmm/openmm/files/8150051/system.pdb.txt)
[works_nve.py.txt](https://github.com/openmm/openmm/files/8150052/works_nve.py.txt)

Below are the output from the various platforms. Usually it seems like the energy terms spike massively just before it crashes, but I don't understand why the order of the .xml files would do this.

Thanks,

Gareth

### Reference:
`#"Step","Potential Energy (kJ/mole)","Kinetic Energy (kJ/mole)","Total Energy (kJ/mole)","Temperature (K)","Box Volume (nm^3)","Density (g/mL)"`
...
`100,-53.494514506154964,27.039465172753232,-26.455049333401732,433.6133540563737,1.030301,0.07259270846536811
110,-27.236485666785786,3.3086763563983093,-23.927809310387477,53.05897299442808,1.030301,0.07259270846536811
Segmentation fault (core dumped)`
### OpenCL:
`100,-53.23412222478068,26.82700063288212,-26.40712159189856,430.2062059799246,1.030301,0.07259270846536811
110,-32.92316336247599,6.56602069735527,-26.35714266512072,105.29476967069344,1.030301,0.07259270846536811
120,1.4365499225153265e+23,31547775385600.0,1.4365499228308042e+23,505910032264674.25,1.030301,0.07259270846536811
130,9.812762097322067e+28,904854700294144.0,9.812762097322157e+28,1.4510534103447568e+16,1.030301,0.07259270846536811
140,1.4205908385270204e+31,3358057606152192.0,1.4205908385270207e+31,5.385086622147509e+16,1.030301,0.07259270846536811
150,2.933014196877673e+32,6956549875433472.0,2.933014196877673e+32,1.1155741819874218e+17,1.030301,0.07259270846536811`

`Traceback (most recent call last):
  File "/lustre/rcghead1/users/c3162723/testing/ommTesting/newParamTests/formamide_version_testing/single_molecule/nve/opencl/./fails_nve.py", line 32, in <module>
    simulation.step(600000)
  File "/home/c3162723/software/src/my_openmm_fork/install/gnu8dev/28Feb22/pythonInstall/lib/python3.9/site-packages/openmm/app/simulation.py", line 141, in step
    self._simulate(endStep=self.currentStep+steps)
  File "/home/c3162723/software/src/my_openmm_fork/install/gnu8dev/28Feb22/pythonInstall/lib/python3.9/site-packages/openmm/app/simulation.py", line 210, in _simulate
    self.integrator.step(stepsToGo)
  File "/home/c3162723/software/src/my_openmm_fork/install/gnu8dev/28Feb22/pythonInstall/lib/python3.9/site-packages/openmm/openmm.py", line 11636, in step
    return _openmm.VerletIntegrator_step(self, steps)
openmm.OpenMMException: clWaitForEvents`

### CUDA:
`90,-40.2270726642338,13.860071877017617,-26.367000787216185,222.26446476138204,1.030301,0.07259270846536811
100,-53.23090262028849,26.823644541203976,-26.407258079084514,430.15238663994205,1.030301,0.07259270846536811
110,-32.94177145573771,6.584286147728562,-26.357485308009146,105.58768016833501,1.030301,0.07259270846536811
120,6.239121191292798e+23,33425676472320.0,6.239121191627055e+23,536024643762955.44,1.030301,0.07259270846536811
130,4.826764310943093e+28,645174452781056.0,4.826764310943157e+28,1.0346220113250622e+16,1.030301,0.07259270846536811
140,7.7064977143566e+30,2938203698364416.0,7.706497714356603e+30,4.711795711967144e+16,1.030301,0.07259270846536811`

`Traceback (most recent call last):
  File "/lustre/rcghead1/users/c3162723/testing/ommTesting/newParamTests/formamide_version_testing/single_molecule/nve/cuda/./fails_nve.py", line 32, in <module>
    simulation.step(600000)
  File "/home/c3162723/software/src/my_openmm_fork/install/gnu8dev/28Feb22/pythonInstall/lib/python3.9/site-packages/openmm/app/simulation.py", line 141, in step
    self._simulate(endStep=self.currentStep+steps)
  File "/home/c3162723/software/src/my_openmm_fork/install/gnu8dev/28Feb22/pythonInstall/lib/python3.9/site-packages/openmm/app/simulation.py", line 243, in _simulate
    self._generate_reports(unwrapped, False)
  File "/home/c3162723/software/src/my_openmm_fork/install/gnu8dev/28Feb22/pythonInstall/lib/python3.9/site-packages/openmm/app/simulation.py", line 263, in _generate_reports
    reporter.report(self, state)
  File "/home/c3162723/software/src/my_openmm_fork/install/gnu8dev/28Feb22/pythonInstall/lib/python3.9/site-packages/openmm/app/statedatareporter.py", line 206, in report
    self._checkForErrors(simulation, state)
  File "/home/c3162723/software/src/my_openmm_fork/install/gnu8dev/28Feb22/pythonInstall/lib/python3.9/site-packages/openmm/app/statedatareporter.py", line 361, in _checkForErrors
    raise ValueError('Energy is NaN')
ValueError: Energy is NaN`

### CPU:
`80,-32.70232472144512,6.305932946930092,-26.39639177451503,101.12392083583997,1.030301,0.07259270846536811
90,-43.12688017527704,16.725968222782672,-26.400911952494365,268.2228784698495,1.030301,0.07259270846536811
100,-53.494514506154964,27.039465172753232,-26.455049333401732,433.6133540563737,1.030301,0.07259270846536811
110,-27.236485666785786,3.3086763563983093,-23.927809310387477,53.05897299442808,1.030301,0.07259270846536811
Segmentation fault      (core dumped)`

## 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

diff --git a/wrappers/python/openmm/app/forcefield.py b/wrappers/python/openmm/app/forcefield.py
index 19c69a6735..897678a878 100644
--- a/wrappers/python/openmm/app/forcefield.py
+++ b/wrappers/python/openmm/app/forcefield.py
@@ -2057,6 +2057,11 @@ def parseElement(element, ff):
             generator.registerAngle(angle.attrib)

     def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
+        pass
+
+    def postprocessSystem(self, sys, data, args):
+        # We need to wait until after all bonds have been added so their lengths will be set correctly.
+
         existing = [f for f in sys.getForces() if type(f) == mm.HarmonicAngleForce]
         if len(existing) == 0:
             force = mm.HarmonicAngleForce()
@@ -3733,8 +3738,11 @@ def parseElement(element, forceField):
     #=============================================================================================

     def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
+        self._nonbondedMethod = nonbondedMethod
+        self._nonbondedCutoff = nonbondedCutoff

-        # get force
+    def postprocessSystem(self, sys, data, args):
+        # We need to wait until after all bonds have been added so their lengths will be set correctly.

         energy = """k*(theta^2 + %s*theta^3 + %s*theta^4 + %s*theta^5 + %s*theta^6);
                     theta = %.15g*pointangle(x2, y2, z2, x4, y4, z4, projx, projy, projz);
@@ -3857,12 +3865,12 @@ def addBond(particles):

         for force in self.forceField._forces:
             if (force.__class__.__name__ == 'AmoebaAngleGenerator'):
-                force.createForcePostOpBendAngle(sys, data, nonbondedMethod, nonbondedCutoff, angles, args)
-                force.createForcePostOpBendInPlaneAngle(sys, data, nonbondedMethod, nonbondedCutoff, angles, args)
+                force.createForcePostOpBendAngle(sys, data, self._nonbondedMethod, self._nonbondedCutoff, angles, args)
+                force.createForcePostOpBendInPlaneAngle(sys, data, self._nonbondedMethod, self._nonbondedCutoff, angles, args)

         for force in self.forceField._forces:
             if (force.__class__.__name__ == 'AmoebaStretchBendGenerator'):
-                force.createForcePostAmoebaBondForce(sys, data, nonbondedMethod, nonbondedCutoff, angles, args)
+                force.createForcePostAmoebaBondForce(sys, data, self._nonbondedMethod, self._nonbondedCutoff, angles, args)

 parsers["AmoebaOutOfPlaneBendForce"] = AmoebaOutOfPlaneBendGenerator.parseElement

@@ -4672,7 +4680,6 @@ def createForcePostAmoebaBondForce(self, sys, data, nonbondedMethod, nonbondedCu
                             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

diff --git a/wrappers/python/tests/TestForceField.py b/wrappers/python/tests/TestForceField.py
index f78445af2f..a0f83b909b 100644
--- a/wrappers/python/tests/TestForceField.py
+++ b/wrappers/python/tests/TestForceField.py
@@ -1020,7 +1020,7 @@ def test_ImpropersOrdering(self):
         system1 = ff1.createSystem(pdb.topology)
         system2 = ff2.createSystem(pdb.topology)

-        imp1 = system1.getForce(2).getTorsionParameters(158)
+        imp1 = system1.getForce(1).getTorsionParameters(158)
         imp2 = system2.getForce(0).getTorsionParameters(158)

         system1_indexes = [imp1[0], imp1[1], imp1[2], imp1[3]]


Do not add constraints involving extra particles (openmm/openmm#3506)

TIP4P waters not compatible with AllBonds?

Hi All,

I'm attempting to run a system with TIP4Pew waters using the "AllBond" constraint option, but I get the following error:
```
openmm.OpenMMException: A constraint cannot involve a massless particle
```
I'm assuming this is because of the virtual site on the water molecules.

Here is the script I'm using:
```
import simtk.openmm.app as app
import simtk.openmm as mm
import simtk.unit as unit
from sys import stdout

import numpy as np

prmtop = app.AmberPrmtopFile('tryp_ben.prmtop')
inpcrd = app.AmberInpcrdFile('tryp_ben.inpcrd')
system = prmtop.createSystem(nonbondedMethod=app.PME, nonbondedCutoff=1*unit.nanometer,
        constraints=app.AllBonds)

num_steps = 10000
time_step = 0.002

input_file = "tryp_ben_new.pdb"

mypdb = app.PDBFile(input_file)

integrator = mm.LangevinIntegrator(298.15*unit.kelvin, 1/unit.picosecond, time_step*unit.picoseconds)
platform = mm.Platform.getPlatformByName('CUDA')
properties = {'CudaDeviceIndex': '0', 'CudaPrecision': 'mixed'}
simulation = app.Simulation(prmtop.topology, system, integrator, platform, properties)
simulation.context.setPositions(mypdb.positions)
simulation.context.setPeriodicBoxVectors(*inpcrd.boxVectors)
simulation.minimizeEnergy()
simulation.context.setVelocitiesToTemperature(298.15 * unit.kelvin)
```

I've attached the files one can use to recreate this error.
[tryp_ben.prmtop.zip](https://github.com/openmm/openmm/files/8181237/tryp_ben.prmtop.zip)
[tryp_ben.inpcrd.zip](https://github.com/openmm/openmm/files/8181238/tryp_ben.inpcrd.zip)

## 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

diff --git a/wrappers/python/openmm/app/internal/amber_file_parser.py b/wrappers/python/openmm/app/internal/amber_file_parser.py
index 26832fe589..e36152a962 100644
--- a/wrappers/python/openmm/app/internal/amber_file_parser.py
+++ b/wrappers/python/openmm/app/internal/amber_file_parser.py
@@ -768,15 +768,18 @@ def readAmberSystem(topology, prmtop_filename=None, prmtop_loader=None, shake=No

     # Add constraints.
     isWater = [prmtop.getResidueLabel(i) in ('WAT', 'HOH', 'TP4', 'TP5', 'T4E') for i in range(prmtop.getNumAtoms())]
+    isEP = [a.element is None for a in topology.atoms()]
     if shake in ('h-bonds', 'all-bonds', 'h-angles'):
         for (iAtom, jAtom, k, rMin) in prmtop.getBondsWithH():
-            system.addConstraint(iAtom, jAtom, rMin)
+            if not (isEP[iAtom] or isEP[jAtom]):
+                system.addConstraint(iAtom, jAtom, rMin)
     if shake in ('all-bonds', 'h-angles'):
         for (iAtom, jAtom, k, rMin) in prmtop.getBondsNoH():
-            system.addConstraint(iAtom, jAtom, rMin)
+            if not (isEP[iAtom] or isEP[jAtom]):
+                system.addConstraint(iAtom, jAtom, rMin)
     if rigidWater and shake is None:
         for (iAtom, jAtom, k, rMin) in prmtop.getBondsWithH():
-            if isWater[iAtom] and isWater[jAtom]:
+            if isWater[iAtom] and isWater[jAtom] and not (isEP[iAtom] or isEP[jAtom]):
                 system.addConstraint(iAtom, jAtom, rMin)

     # Add harmonic bonds.

diff --git a/wrappers/python/tests/TestAmberPrmtopFile.py b/wrappers/python/tests/TestAmberPrmtopFile.py
index 4473a2d86b..b107e9dede 100644
--- a/wrappers/python/tests/TestAmberPrmtopFile.py
+++ b/wrappers/python/tests/TestAmberPrmtopFile.py
@@ -448,5 +448,14 @@ def testAmberCMAP(self):
                 OpenMM_CMAP_E = simulation.context.getState(getEnergy=True, groups=1<<i).getPotentialEnergy().value_in_unit(kilojoules_per_mole)/conversion
                 self.assertAlmostEqual(OpenMM_CMAP_E, sander_CMAP_E, places=4)

+    def testEPConstraints(self):
+        """Test different types of constraints when using extra particles"""
+        prmtop = AmberPrmtopFile('systems/peptide_with_tip4p.prmtop')
+        for constraints in (HBonds, AllBonds):
+            system = prmtop.createSystem(constraints=constraints)
+            integrator = VerletIntegrator(0.001*picoseconds)
+            # If a constraint was added to a massless particle, this will throw an exception.
+            context = Context(system, integrator, Platform.getPlatformByName('Reference'))
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/wrappers/python/tests/systems/peptide_with_tip4p.inpcrd b/wrappers/python/tests/systems/peptide_with_tip4p.inpcrd
new file mode 100644
index 0000000000..99127dab38
--- /dev/null
+++ b/wrappers/python/tests/systems/peptide_with_tip4p.inpcrd
@@ -0,0 +1,46 @@
+default_name
+    85
+   6.1027258   9.4114116   5.1039335   6.5815895  10.1277022   5.6290157
+   6.6678073   9.1230379   4.3177487   5.2086444   9.7657205   4.7621357
+   5.8010133   8.2419298   5.9556322   5.2338195   8.5904194   6.8193516
+   4.9060379   7.2614871   5.1978254   5.2290030   7.1815685   4.1573041
+   4.9489259   6.2745706   5.6619402   3.4965842   7.8237994   5.2678187
+   3.3776621   8.9780360   4.7980651   2.6472232   7.1669020   5.8997283
+   7.0376422   7.5370608   6.4781338   7.9157036   7.1622066   5.7047041
+   7.0652775   7.3158858   7.7952036   6.3169315   7.6633401   8.3789911
+   8.0764491   6.4982978   8.4537730   9.0634999   6.8670277   8.1688606
+   7.9197212   6.6666247   9.9695324   8.6690479   6.0648171  10.4845540
+   8.0657682   7.7128438  10.2415669   6.9244697   6.3515483  10.2875817
+   7.9931502   5.0209894   8.0274701   9.0297108   4.4106384   7.7836349
+   6.9039267   4.4598555   7.9317597   4.8638450  11.1831355   9.8736302
+   4.1346516  10.5621898   9.8644378   4.8794334  11.5468847   8.9890396
+   4.7726236  11.1496591   9.7590789   2.6329636   2.9920417   7.3605727
+   2.8338815   2.3735456   6.6584157   3.4695441   3.4182530   7.5500773
+   2.7657541   2.9679551   7.2948118  11.6032547   4.2363825   3.6652327
+  11.0489985   4.5413440   2.9475194  11.9080957   3.3741621   3.3823900
+  11.5709231   4.1649390   3.5372464   2.6699140   9.6787405   9.4896712
+   2.7570939   9.4003152   8.5789176   2.7420827   8.8695924   9.9959596
+   2.6901212   9.5395278   9.4380524   4.2027790   3.2218855   3.8639297
+   3.7166501   3.1773864   3.0401503   5.0820834   2.9173323   3.6390697
+   4.2524311   3.1769782   3.7295794   0.5083146   4.1661638   6.2786993
+   1.2617567   3.6970865   6.6372025   0.8431777   4.5870678   5.4874468
+   0.6474560   4.1596318   6.2235450   5.4377311   1.1104254   9.6028083
+   5.7316024   1.7256555   8.9303498   4.4874126   1.2190194   9.6268500
+   5.3534380   1.2030977   9.5200769  12.5431809   8.9055182   5.2286458
+  11.6760008   9.0337082   4.8468081  12.3913378   8.3331541   5.9810074
+  12.4132771   8.8479552   5.2760220  10.2580286  10.2519211   7.1265204
+  10.2459042  10.2237519   6.1698049  10.7147126  11.0667847   7.3358240
+  10.3146089  10.3527584   7.0310610   3.0018903   7.5970825  11.3670397
+   2.8021272   6.7560910  10.9540893   2.7438148   7.4819565  12.2820359
+   2.9430006   7.4746080  11.4306793   6.9630906  12.5058599   7.3124894
+   6.0324019  12.6912046   7.4397686   7.3972580  13.3452184   7.4595676
+   6.8995820  12.6364993   7.3478448   2.2680782   5.1300381  10.2420328
+   1.8500766   5.0630853  11.1004605   1.5463903   5.2949704   9.6353352
+   2.1225859   5.1422855  10.2745597  11.0871036   7.0226771   2.3181943
+  10.9127438   7.6505629   1.6181586  10.4907008   7.2778323   3.0231798
+  10.9883767   7.1353536   2.3196085   8.9439794   4.4033560   2.5175984
+   8.7517218   4.6458555   1.6110875   8.3729800   3.6546287   2.6915467
+   8.8458299   4.3392611   2.4235532  11.9946982   8.0588113   7.7954434
+  12.4305976   8.4597111   8.5478050  11.2851347   8.6646517   7.5790687
+  11.9594798   8.1878177   7.8647399
+  13.0633205  13.0633205  13.0633205 109.4712190 109.4712190 109.4712190
diff --git a/wrappers/python/tests/systems/peptide_with_tip4p.prmtop b/wrappers/python/tests/systems/peptide_with_tip4p.prmtop
new file mode 100644
index 0000000000..8d01f9df13
--- /dev/null
+++ b/wrappers/python/tests/systems/peptide_with_tip4p.prmtop
@@ -0,0 +1,431 @@
+%VERSION  VERSION_STAMP = V0001.000  DATE = 03/14/22  14:16:58
+%FLAG TITLE
+%FORMAT(20a4)
+default_name
+%FLAG POINTERS
+%FORMAT(10I8)
+      85      11      56      28      25      17      41      40       0       0
+     228      17      28      17      40      19      30      25      16       1
+       0       0       0       0       0       0       0       2      14       0
+      15
+%FLAG ATOM_NAME
+%FORMAT(20a4)
+N   H1  H2  H3  CA  HA  CB  HB2 HB3 CG  OD1 OD2 C   O   N   H   CA  HA  CB  HB1
+HB2 HB3 C   O   OXT O   H1  H2  EPW O   H1  H2  EPW O   H1  H2  EPW O   H1  H2
+EPW O   H1  H2  EPW O   H1  H2  EPW O   H1  H2  EPW O   H1  H2  EPW O   H1  H2
+EPW O   H1  H2  EPW O   H1  H2  EPW O   H1  H2  EPW O   H1  H2  EPW O   H1  H2
+EPW O   H1  H2  EPW
+%FLAG CHARGE
+%FORMAT(5E16.8)
+  1.42498386E+00  4.00890600E+00  4.00890600E+00  4.00890600E+00  5.32091160E-01
+  2.07916443E+00 -4.28224050E-01 -3.07956870E-01 -3.07956870E-01  1.49313526E+01
+ -1.47309073E+01 -1.47309073E+01  1.02427548E+01 -1.07311125E+01 -6.96274083E+00
+  4.88539863E+00 -3.18343581E+00  1.94431941E+00 -3.81392739E+00  1.39218372E+00
+  1.39218372E+00  1.39218372E+00  1.40876601E+01 -1.46780627E+01 -1.46780627E+01
+  0.00000000E+00  9.55249411E+00  9.55249411E+00 -1.91049882E+01  0.00000000E+00
+  9.55249411E+00  9.55249411E+00 -1.91049882E+01  0.00000000E+00  9.55249411E+00
+  9.55249411E+00 -1.91049882E+01  0.00000000E+00  9.55249411E+00  9.55249411E+00
+ -1.91049882E+01  0.00000000E+00  9.55249411E+00  9.55249411E+00 -1.91049882E+01
+  0.00000000E+00  9.55249411E+00  9.55249411E+00 -1.91049882E+01  0.00000000E+00
+  9.55249411E+00  9.55249411E+00 -1.91049882E+01  0.00000000E+00  9.55249411E+00
+  9.55249411E+00 -1.91049882E+01  0.00000000E+00  9.55249411E+00  9.55249411E+00
+ -1.91049882E+01  0.00000000E+00  9.55249411E+00  9.55249411E+00 -1.91049882E+01
+  0.00000000E+00  9.55249411E+00  9.55249411E+00 -1.91049882E+01  0.00000000E+00
+  9.55249411E+00  9.55249411E+00 -1.91049882E+01  0.00000000E+00  9.55249411E+00
+  9.55249411E+00 -1.91049882E+01  0.00000000E+00  9.55249411E+00  9.55249411E+00
+ -1.91049882E+01  0.00000000E+00  9.55249411E+00  9.55249411E+00 -1.91049882E+01
+%FLAG ATOMIC_NUMBER
+%FORMAT(10I8)
+       7       1       1       1       6       1       6       1       1       6
+       8       8       6       8       7       1       6       1       6       1
+       1       1       6       8       8       8       1       1       0       8
+       1       1       0       8       1       1       0       8       1       1
+       0       8       1       1       0       8       1       1       0       8
+       1       1       0       8       1       1       0       8       1       1
+       0       8       1       1       0       8       1       1       0       8
+       1       1       0       8       1       1       0       8       1       1
+       0       8       1       1       0
+%FLAG MASS
+%FORMAT(5E16.8)
+  1.40100000E+01  1.00800000E+00  1.00800000E+00  1.00800000E+00  1.20100000E+01
+  1.00800000E+00  1.20100000E+01  1.00800000E+00  1.00800000E+00  1.20100000E+01
+  1.60000000E+01  1.60000000E+01  1.20100000E+01  1.60000000E+01  1.40100000E+01
+  1.00800000E+00  1.20100000E+01  1.00800000E+00  1.20100000E+01  1.00800000E+00
+  1.00800000E+00  1.00800000E+00  1.20100000E+01  1.60000000E+01  1.60000000E+01
+  1.60000000E+01  1.00800000E+00  1.00800000E+00  0.00000000E+00  1.60000000E+01
+  1.00800000E+00  1.00800000E+00  0.00000000E+00  1.60000000E+01  1.00800000E+00
+  1.00800000E+00  0.00000000E+00  1.60000000E+01  1.00800000E+00  1.00800000E+00
+  0.00000000E+00  1.60000000E+01  1.00800000E+00  1.00800000E+00  0.00000000E+00
+  1.60000000E+01  1.00800000E+00  1.00800000E+00  0.00000000E+00  1.60000000E+01
+  1.00800000E+00  1.00800000E+00  0.00000000E+00  1.60000000E+01  1.00800000E+00
+  1.00800000E+00  0.00000000E+00  1.60000000E+01  1.00800000E+00  1.00800000E+00
+  0.00000000E+00  1.60000000E+01  1.00800000E+00  1.00800000E+00  0.00000000E+00
+  1.60000000E+01  1.00800000E+00  1.00800000E+00  0.00000000E+00  1.60000000E+01
+  1.00800000E+00  1.00800000E+00  0.00000000E+00  1.60000000E+01  1.00800000E+00
+  1.00800000E+00  0.00000000E+00  1.60000000E+01  1.00800000E+00  1.00800000E+00
+  0.00000000E+00  1.60000000E+01  1.00800000E+00  1.00800000E+00  0.00000000E+00
+%FLAG ATOM_TYPE_INDEX
+%FORMAT(10I8)
+       1       2       2       2       3       4       3       5       5       6
+       7       7       6       7       1       2       3       8       3       5
+       5       5       6       7       7       9      10      10      11       9
+      10      10      11       9      10      10      11       9      10      10
+      11       9      10      10      11       9      10      10      11       9
+      10      10      11       9      10      10      11       9      10      10
+      11       9      10      10      11       9      10      10      11       9
+      10      10      11       9      10      10      11       9      10      10
+      11       9      10      10      11
+%FLAG NUMBER_EXCLUDED_ATOMS
+%FORMAT(10I8)
+      12       6       5       4      12       7       8       5       4       3
+       1       1       7       3      10       4       8       7       6       3
+       2       1       2       1       1       3       2       1       1       3
+       2       1       1       3       2       1       1       3       2       1
+       1       3       2       1       1       3       2       1       1       3
+       2       1       1       3       2       1       1       3       2       1
+       1       3       2       1       1       3       2       1       1       3
+       2       1       1       3       2       1       1       3       2       1
+       1       3       2       1       1
+%FLAG NONBONDED_PARM_INDEX
+%FORMAT(10I8)
+       1       2       4       7      11      16      22      29      37      46
+      56       2       3       5       8      12      17      23      30      38
+      47      57       4       5       6       9      13      18      24      31
+      39      48      58       7       8       9      10      14      19      25
+      32      40      49      59      11      12      13      14      15      20
+      26      33      41      50      60      16      17      18      19      20
+      21      27      34      42      51      61      22      23      24      25
+      26      27      28      35      43      52      62      29      30      31
+      32      33      34      35      36      44      53      63      37      38
+      39      40      41      42      43      44      45      -1      64      46
+      47      48      49      50      51      52      53      -1      55      65
+      56      57      58      59      60      61      62      63      64      65
+      66
+%FLAG RESIDUE_LABEL
+%FORMAT(20a4)
+ASP ALA WAT WAT WAT WAT WAT WAT WAT WAT WAT WAT WAT WAT WAT WAT WAT
+%FLAG RESIDUE_POINTER
+%FORMAT(10I8)
+       1      15      26      30      34      38      42      46      50      54
+      58      62      66      70      74      78      82
+%FLAG BOND_FORCE_CONSTANT
+%FORMAT(5E16.8)
+  5.70000000E+02  4.90000000E+02  6.56000000E+02  3.40000000E+02  3.17000000E+02
+  3.40000000E+02  3.10000000E+02  3.17000000E+02  4.34000000E+02  3.67000000E+02
+  6.56000000E+02  3.40000000E+02  3.40000000E+02  3.10000000E+02  4.34000000E+02
+  3.37000000E+02  5.53000000E+02  5.53000000E+02  5.53000000E+02
+%FLAG BOND_EQUIL_VALUE
+%FORMAT(5E16.8)
+  1.22900000E+00  1.33500000E+00  1.25000000E+00  1.09000000E+00  1.52200000E+00
+  1.09000000E+00  1.52600000E+00  1.52200000E+00  1.01000000E+00  1.47100000E+00
+  1.25000000E+00  1.09000000E+00  1.09000000E+00  1.52600000E+00  1.01000000E+00
+  1.44900000E+00  9.57200000E-01  1.51360000E+00  1.25000000E-01
+%FLAG ANGLE_FORCE_CONSTANT
+%FORMAT(5E16.8)
+  8.00000000E+01  5.00000000E+01  5.00000000E+01  8.00000000E+01  5.00000000E+01
+  3.50000000E+01  6.30000000E+01  7.00000000E+01  5.00000000E+01  5.00000000E+01
+  5.00000000E+01  6.30000000E+01  8.00000000E+01  7.00000000E+01  5.00000000E+01
+  3.50000000E+01  5.00000000E+01  8.00000000E+01  8.00000000E+01  8.00000000E+01
+  3.50000000E+01  6.30000000E+01  5.00000000E+01  5.00000000E+01  5.00000000E+01
+  7.00000000E+01  5.00000000E+01  5.00000000E+01  8.00000000E+01  6.30000000E+01
+%FLAG ANGLE_EQUIL_VALUE
+%FORMAT(5E16.8)
+  2.14501057E+00  2.09439600E+00  2.12755727E+00  2.19911580E+00  1.91113635E+00
+  1.91113635E+00  1.93906163E+00  2.04203610E+00  1.91113635E+00  1.91113635E+00
+  1.91113635E+00  1.93906163E+00  2.10137732E+00  2.03505478E+00  1.91113635E+00
+  1.91113635E+00  1.91113635E+00  1.94080696E+00  1.94080696E+00  2.19911580E+00
+  1.91113635E+00  1.93906163E+00  1.91113635E+00  1.91113635E+00  1.91113635E+00
+  2.04203610E+00  2.06018753E+00  1.91113635E+00  1.91462701E+00  1.92160833E+00
+%FLAG DIHEDRAL_FORCE_CONSTANT
+%FORMAT(5E16.8)
+  2.00000000E+00  2.50000000E+00  0.00000000E+00  1.80000000E+00  8.00000000E-01
+  0.00000000E+00  0.00000000E+00  2.70000000E-01  4.20000000E-01  4.24000000E-01
+  4.59000000E-01  5.80000000E-02  1.54000000E-01  1.55555556E-01  2.00000000E-01
+  2.00000000E-01  4.00000000E-01  7.69000000E-01  0.00000000E+00  3.10000000E-02
+  2.15400000E+00  6.47000000E-01  8.90000000E-02  1.05000000E+01  1.10000000E+00
+%FLAG DIHEDRAL_PERIODICITY
+%FORMAT(5E16.8)
+  1.00000000E+00  2.00000000E+00  2.00000000E+00  2.00000000E+00  3.00000000E+00
+  4.00000000E+00  1.00000000E+00  2.00000000E+00  3.00000000E+00  1.00000000E+00
+  2.00000000E+00  3.00000000E+00  4.00000000E+00  3.00000000E+00  1.00000000E+00
+  2.00000000E+00  3.00000000E+00  2.00000000E+00  3.00000000E+00  4.00000000E+00
+  1.00000000E+00  2.00000000E+00  4.00000000E+00  2.00000000E+00  2.00000000E+00
+%FLAG DIHEDRAL_PHASE
+%FORMAT(5E16.8)
+  0.00000000E+00  3.14159400E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  3.14159400E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  3.14159400E+00  0.00000000E+00  3.14159400E+00
+  3.14159400E+00  3.14159400E+00  0.00000000E+00  3.14159400E+00  3.14159400E+00
+%FLAG SCEE_SCALE_FACTOR
+%FORMAT(5E16.8)
+  1.20000000E+00  1.20000000E+00  1.20000000E+00  1.20000000E+00  1.20000000E+00
+  1.20000000E+00  1.20000000E+00  1.20000000E+00  1.20000000E+00  1.20000000E+00
+  1.20000000E+00  1.20000000E+00  1.20000000E+00  1.20000000E+00  1.20000000E+00
+  1.20000000E+00  1.20000000E+00  1.20000000E+00  1.20000000E+00  1.20000000E+00
+  1.20000000E+00  1.20000000E+00  1.20000000E+00  0.00000000E+00  0.00000000E+00
+%FLAG SCNB_SCALE_FACTOR
+%FORMAT(5E16.8)
+  2.00000000E+00  2.00000000E+00  2.00000000E+00  2.00000000E+00  2.00000000E+00
+  2.00000000E+00  2.00000000E+00  2.00000000E+00  2.00000000E+00  2.00000000E+00
+  2.00000000E+00  2.00000000E+00  2.00000000E+00  2.00000000E+00  2.00000000E+00
+  2.00000000E+00  2.00000000E+00  2.00000000E+00  2.00000000E+00  2.00000000E+00
+  2.00000000E+00  2.00000000E+00  2.00000000E+00  0.00000000E+00  0.00000000E+00
+%FLAG SOLTY
+%FORMAT(5E16.8)
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00
+%FLAG LENNARD_JONES_ACOEF
+%FORMAT(5E16.8)
+  9.44293233E+05  2.12601181E+03  1.39982777E-01  9.95480466E+05  2.56678134E+03
+  1.04308023E+06  2.01791425E+04  9.14716912E+00  2.27401052E+04  2.01823541E+02
+  8.96776989E+04  1.07193646E+02  9.71708117E+04  1.41077189E+03  7.51607703E+03
+  8.82619071E+05  2.27577561E+03  9.24822270E+05  2.01619733E+04  8.61541883E+04
+  8.19971662E+05  6.06829342E+05  1.02595236E+03  6.47841731E+05  1.12780457E+04
+  5.44261042E+04  5.74393458E+05  3.79876399E+05  6.20665997E+04  5.94667300E+01
+  6.78771368E+04  8.79040886E+02  4.98586848E+03  6.01816484E+04  3.69471530E+04
+  3.25969625E+03  7.87979792E+05  1.63574730E+03  8.33694635E+05  1.61827331E+04
+  7.36215582E+04  7.39175513E+05  5.02600786E+05  5.06742043E+04  6.56136207E+05
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00
+%FLAG LENNARD_JONES_BCOEF
+%FORMAT(5E16.8)
+  8.01323529E+02  2.09604198E+01  9.37598976E-02  7.36907417E+02  2.06278363E+01
+  6.75612247E+02  6.45756063E+01  7.57919667E-01  6.13981767E+01  3.56012899E+00
+  1.36131731E+02  2.59456373E+00  1.26919150E+02  9.41257003E+00  2.17257828E+01
+  6.53361429E+02  1.82891803E+01  5.99015525E+02  5.44372326E+01  1.12529845E+02
+  5.31102864E+02  6.77220874E+02  1.53505284E+01  6.26720080E+02  5.08951803E+01
+  1.11805549E+02  5.55666448E+02  5.64885984E+02  1.13252061E+02  1.93248820E+00
+  1.06076943E+02  7.42992380E+00  1.76949863E+01  9.40505980E+01  9.21192136E+01
+  1.43076527E+01  7.24069102E+02  1.81862545E+01  6.67064884E+02  5.72019842E+01
+  1.22007818E+02  5.91437209E+02  6.09644486E+02  1.01222865E+02  6.53563058E+02
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00  0.00000000E+00
+  0.00000000E+00
+%FLAG BONDS_INC_HYDROGEN
+%FORMAT(10I8)
+      18      21       4      18      24       4      12      15       6       0
+       3       9       0       6       9       0       9       9      54      57
+      12      54      60      12      54      63      12      48      51      13
+      42      45      15      78      75      17      81      75      17      81
+      78      18      90      87      17      93      87      17      93      90
+      18     102      99      17     105      99      17     105     102      18
+     114     111      17     117     111      17     117     114      18     126
+     123      17     129     123      17     129     126      18     138     135
+      17     141     135      17     141     138      18     150     147      17
+     153     147      17     153     150      18     162     159      17     165
+     159      17     165     162      18     174     171      17     177     171
+      17     177     174      18     186     183      17     189     183      17
+     189     186      18     198     195      17     201     195      17     201
+     198      18     210     207      17     213     207      17     213     210
+      18     222     219      17     225     219      17     225     222      18
+     234     231      17     237     231      17     237     234      18     246
+     243      17     249     243      17     249     246      18
+%FLAG BONDS_WITHOUT_HYDROGEN
+%FORMAT(10I8)
+      36      39       1      36      42       2      27      30       3      27
+      33       3      18      27       5      12      18       7      12      36
+       8       0      12      10      66      69      11      66      72      11
+      48      54      14      48      66       8      42      48      16      84
+      75      19      96      87      19     108      99      19     120     111
+      19     132     123      19     144     135      19     156     147      19
+     168     159      19     180     171      19     192     183      19     204
+     195      19     216     207      19     228     219      19     240     231
+      19     252     243      19
+%FLAG ANGLES_INC_HYDROGEN
+%FORMAT(10I8)
+      36      42      45       2      24      18      27       5      21      18
+      24       6      21      18      27       5      15      12      18       9
+      15      12      36      10      12      18      21      11      12      18
+      24      11       9       0      12      15       6       0       9      16
+       6       0      12      15       3       0       6      16       3       0
+       9      16       3       0      12      15       0      12      15      17
+      60      54      63      21      57      54      60      21      57      54
+      63      21      51      48      54      23      51      48      66      24
+      48      54      57      25      48      54      60      25      48      54
+      63      25      45      42      48      27      42      48      51      28
+%FLAG ANGLES_WITHOUT_HYDROGEN
+%FORMAT(10I8)
+      39      36      42       1      36      42      48       3      30      27
+      33       4      18      12      36       7      18      27      30       8
+      18      27      33       8      12      18      27      12      12      36
+      39      13      12      36      42      14       0      12      18      18
+       0      12      36      19      69      66      72      20      54      48
+      66      22      48      66      69      26      48      66      72      26
+      42      48      54      29      42      48      66      30
+%FLAG DIHEDRALS_INC_HYDROGEN
+%FORMAT(10I8)
+      39      36      42      45       1      39      36     -42      45       2
+      36      42      48      51       3      24      18      12      36      14
+      24      18      27      30       3      24      18      27      33       3
+      21      18      12      36      14      21      18      27      30       3
+      21      18      27      33       3      15      12      18      21      14
+      15      12      18      24      14      15      12      18      27      14
+      15      12      36      39       3      15      12      36      42       3
+      12      36      42      45       2       9       0      12      15      14
+       9       0      12      18      14       9       0      12      36      14
+       6       0      12      15      14       6       0      12      18      14
+       6       0      12      36      14       3       0      12      15      14
+       3       0      12      18      14       3       0      12      36      14
+       0      12      18      21      14       0      12      18      24      14
+      63      54      48      66      14      60      54      48      66      14
+      57      54      48      66      14      51      48      54      57      14
+      51      48      54      60      14      51      48      54      63      14
+      51      48      66      69       3      51      48      66      72       3
+      45      42      48      51       3      45      42      48      54       3
+      45      42      48      66       3      42      48      54      57      14
+      42      48      54      60      14      42      48      54      63      14
+      36      48     -42     -45      25
+%FLAG DIHEDRALS_WITHOUT_HYDROGEN
+%FORMAT(10I8)
+      39      36      42      48       2      36      42      48      54       1
+      36      42     -48      54       4      36      42     -48      54       5
+      36      42     -48      54       6      36      42      48      66       7
+      36      42     -48      66       8      36      42     -48      66       9
+      36      42     -48      66       6      27      18      12      36      10
+      27      18     -12      36      11      27      18     -12      36      12
+      27      18     -12      36      13      18      12      36      39       3
+      18      12      36      42      15      18      12     -36      42      16
+      18      12     -36      42      17      18      12     -36      42       6
+      12      18      27      30       7      12      18     -27      30      18
+      12      18     -27      30      19      12      18     -27      30      20
+      12      18      27      33       7      12      18     -27      33      18
+      12      18     -27      33      19      12      18     -27      33      20
+      12      36      42      48       2       0      12      18      27      21
+       0      12     -18      27      22       0      12     -18      27      12
+       0      12     -18      27      23       0      12      36      39       3
+       0      12      36      42       3      54      48      66      69       3
+      54      48      66      72       3      42      48      66      69       3
+      42      48      66      72       3      12      42     -36     -39      24
+      18      30     -27     -33      24      48      69     -66     -72      24
+%FLAG EXCLUDED_ATOMS_LIST
+%FORMAT(10I8)
+       2       3       4       5       6       7       8       9      10      13
+      14      15       3       4       5       6       7      13       4       5
+       6       7      13       5       6       7      13       6       7       8
+       9      10      11      12      13      14      15      16      17       7
+       8       9      10      13      14      15       8       9      10      11
+      12      13      14      15       9      10      11      12      13      10
+      11      12      13      11      12      13      12       0      14      15
+      16      17      18      19      23      15      16      17      16      17
+      18      19      20      21      22      23      24      25      17      18
+      19      23      18      19      20      21      22      23      24      25
+      19      20      21      22      23      24      25      20      21      22
+      23      24      25      21      22      23      22      23      23      24
+      25      25       0      27      28      29      28      29      29       0
+      31      32      33      32      33      33       0      35      36      37
+      36      37      37       0      39      40      41      40      41      41
+       0      43      44      45      44      45      45       0      47      48
+      49      48      49      49       0      51      52      53      52      53
+      53       0      55      56      57      56      57      57       0      59
+      60      61      60      61      61       0      63      64      65      64
+      65      65       0      67      68      69      68      69      69       0
+      71      72      73      72      73      73       0      75      76      77
+      76      77      77       0      79      80      81      80      81      81
+       0      83      84      85      84      85      85       0
+%FLAG HBOND_ACOEF
+%FORMAT(5E16.8)
+  0.00000000E+00
+%FLAG HBOND_BCOEF
+%FORMAT(5E16.8)
+  0.00000000E+00
+%FLAG HBCUT
+%FORMAT(5E16.8)
+  0.00000000E+00
+%FLAG AMBER_ATOM_TYPE
+%FORMAT(20a4)
+N3  H   H   H   CX  HP  2C  HC  HC  CO  O2  O2  C   O   N   H   CX  H1  CT  HC
+HC  HC  C   O2  O2  OW  HW  HW  EP  OW  HW  HW  EP  OW  HW  HW  EP  OW  HW  HW
+EP  OW  HW  HW  EP  OW  HW  HW  EP  OW  HW  HW  EP  OW  HW  HW  EP  OW  HW  HW
+EP  OW  HW  HW  EP  OW  HW  HW  EP  OW  HW  HW  EP  OW  HW  HW  EP  OW  HW  HW
+EP  OW  HW  HW  EP
+%FLAG TREE_CHAIN_CLASSIFICATION
+%FORMAT(20a4)
+BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA
+BLA BLA BLA BLA BLA M   S   E   E   M   S   E   E   M   S   E   E   M   S   E
+E   M   S   E   E   M   S   E   E   M   S   E   E   M   S   E   E   M   S   E
+E   M   S   E   E   M   S   E   E   M   S   E   E   M   S   E   E   M   S   E
+E   M   S   E   E
+%FLAG JOIN_ARRAY
+%FORMAT(10I8)
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0
+%FLAG IROTAT
+%FORMAT(10I8)
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0       0       0       0       0       0
+       0       0       0       0       0
+%FLAG SOLVENT_POINTERS
+%FORMAT(3I8)
+       2      16       2
+%FLAG ATOMS_PER_MOLECULE
+%FORMAT(10I8)
+      25       4       4       4       4       4       4       4       4       4
+       4       4       4       4       4       4
+%FLAG BOX_DIMENSIONS
+%FORMAT(5E16.8)
+  1.09471219E+02  1.30633205E+01  1.30633205E+01  1.30633205E+01
+%FLAG RADIUS_SET
+%FORMAT(1a80)
+H(N)-modified Bondi radii (mbondi2)
+%FLAG RADII
+%FORMAT(5E16.8)
+  1.55000000E+00  1.30000000E+00  1.30000000E+00  1.30000000E+00  1.70000000E+00
+  1.20000000E+00  1.70000000E+00  1.20000000E+00  1.20000000E+00  1.70000000E+00
+  1.50000000E+00  1.50000000E+00  1.70000000E+00  1.50000000E+00  1.55000000E+00
+  1.30000000E+00  1.70000000E+00  1.20000000E+00  1.70000000E+00  1.20000000E+00
+  1.20000000E+00  1.20000000E+00  1.70000000E+00  1.50000000E+00  1.50000000E+00
+  1.50000000E+00  1.20000000E+00  1.20000000E+00  1.50000000E+00  1.50000000E+00
+  1.20000000E+00  1.20000000E+00  1.50000000E+00  1.50000000E+00  1.20000000E+00
+  1.20000000E+00  1.50000000E+00  1.50000000E+00  1.20000000E+00  1.20000000E+00
+  1.50000000E+00  1.50000000E+00  1.20000000E+00  1.20000000E+00  1.50000000E+00
+  1.50000000E+00  1.20000000E+00  1.20000000E+00  1.50000000E+00  1.50000000E+00
+  1.20000000E+00  1.20000000E+00  1.50000000E+00  1.50000000E+00  1.20000000E+00
+  1.20000000E+00  1.50000000E+00  1.50000000E+00  1.20000000E+00  1.20000000E+00
+  1.50000000E+00  1.50000000E+00  1.20000000E+00  1.20000000E+00  1.50000000E+00
+  1.50000000E+00  1.20000000E+00  1.20000000E+00  1.50000000E+00  1.50000000E+00
+  1.20000000E+00  1.20000000E+00  1.50000000E+00  1.50000000E+00  1.20000000E+00
+  1.20000000E+00  1.50000000E+00  1.50000000E+00  1.20000000E+00  1.20000000E+00
+  1.50000000E+00  1.50000000E+00  1.20000000E+00  1.20000000E+00  1.50000000E+00
+%FLAG SCREEN
+%FORMAT(5E16.8)
+  7.90000000E-01  8.50000000E-01  8.50000000E-01  8.50000000E-01  7.20000000E-01
+  8.50000000E-01  7.20000000E-01  8.50000000E-01  8.50000000E-01  7.20000000E-01
+  8.50000000E-01  8.50000000E-01  7.20000000E-01  8.50000000E-01  7.90000000E-01
+  8.50000000E-01  7.20000000E-01  8.50000000E-01  7.20000000E-01  8.50000000E-01
+  8.50000000E-01  8.50000000E-01  7.20000000E-01  8.50000000E-01  8.50000000E-01
+  8.50000000E-01  8.50000000E-01  8.50000000E-01  8.00000000E-01  8.50000000E-01
+  8.50000000E-01  8.50000000E-01  8.00000000E-01  8.50000000E-01  8.50000000E-01
+  8.50000000E-01  8.00000000E-01  8.50000000E-01  8.50000000E-01  8.50000000E-01
+  8.00000000E-01  8.50000000E-01  8.50000000E-01  8.50000000E-01  8.00000000E-01
+  8.50000000E-01  8.50000000E-01  8.50000000E-01  8.00000000E-01  8.50000000E-01
+  8.50000000E-01  8.50000000E-01  8.00000000E-01  8.50000000E-01  8.50000000E-01
+  8.50000000E-01  8.00000000E-01  8.50000000E-01  8.50000000E-01  8.50000000E-01
+  8.00000000E-01  8.50000000E-01  8.50000000E-01  8.50000000E-01  8.00000000E-01
+  8.50000000E-01  8.50000000E-01  8.50000000E-01  8.00000000E-01  8.50000000E-01
+  8.50000000E-01  8.50000000E-01  8.00000000E-01  8.50000000E-01  8.50000000E-01
+  8.50000000E-01  8.00000000E-01  8.50000000E-01  8.50000000E-01  8.50000000E-01
+  8.00000000E-01  8.50000000E-01  8.50000000E-01  8.50000000E-01  8.00000000E-01
+%FLAG IPOL
+%FORMAT(1I8)
+       0


Max Drude distance defaults to 0.02 nm (openmm/openmm#3508)

Max Drude distance defaults to 0.02 nm

### Issue Description

**Problem:**
The default maximum distance a Drude particle can move from its parent particle is not explicitly defined, leading to potential confusion for users. Previously, the default value was set to 0, which disabled the hard wall constraint. This lack of clarity could result in unintended behavior or instability in simulations that rely on Drude integrators.

**Expected Behavior:**
The default maximum distance for Drude particles should be explicitly defined as 0.02 nm. This ensures that the hard wall constraint is enabled by default, improving simulation stability and providing a clear, consistent behavior for users. Users should be able to override this default value if needed, but the system should default to the defined constraint for better usability and reliability.

## 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

diff --git a/plugins/drude/openmmapi/include/openmm/DrudeIntegrator.h b/plugins/drude/openmmapi/include/openmm/DrudeIntegrator.h
index 2fc0a95a28..628dcfd38f 100644
--- a/plugins/drude/openmmapi/include/openmm/DrudeIntegrator.h
+++ b/plugins/drude/openmmapi/include/openmm/DrudeIntegrator.h
@@ -9,7 +9,7 @@
  * Biological Structures at Stanford, funded under the NIH Roadmap for        *
  * Medical Research, grant U54 GM072970. See https://simtk.org.               *
  *                                                                            *
- * Portions copyright (c) 2008-2021 Stanford University and the Authors.      *
+ * Portions copyright (c) 2008-2022 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -72,12 +72,12 @@ class OPENMM_EXPORT_DRUDE DrudeIntegrator : public Integrator {
     void setDrudeTemperature(double temp);
     /**
      * Get the maximum distance a Drude particle can ever move from its parent particle, measured in nm.  This is implemented
-     * with a hard wall constraint.  If this distance is set to 0 (the default), the hard wall constraint is omitted.
+     * with a hard wall constraint.  The default value is 0.02.  If this distance is set to 0, the hard wall constraint is omitted.
      */
     double getMaxDrudeDistance() const;
     /**
      * Set the maximum distance a Drude particle can ever move from its parent particle, measured in nm.  This is implemented
-     * with a hard wall constraint.  If this distance is set to 0 (the default), the hard wall constraint is omitted.
+     * with a hard wall constraint.  The default value is 0.02.  If this distance is set to 0, the hard wall constraint is omitted.
      */
     void setMaxDrudeDistance(double distance);
     /**
diff --git a/plugins/drude/openmmapi/include/openmm/DrudeLangevinIntegrator.h b/plugins/drude/openmmapi/include/openmm/DrudeLangevinIntegrator.h
index 8c5083b798..8f39c6e075 100644
--- a/plugins/drude/openmmapi/include/openmm/DrudeLangevinIntegrator.h
+++ b/plugins/drude/openmmapi/include/openmm/DrudeLangevinIntegrator.h
@@ -47,7 +47,7 @@ namespace OpenMM {
  *
  * This integrator can optionally set an upper limit on how far any Drude particle is ever allowed to
  * get from its parent particle.  This can sometimes help to improve stability.  The limit is enforced
- * with a hard wall constraint.
+ * with a hard wall constraint.  By default the limit is set to 0.02 nm.
  *
  * This Integrator requires the System to include a DrudeForce, which it uses to identify the Drude
  * particles.
diff --git a/plugins/drude/openmmapi/include/openmm/DrudeNoseHooverIntegrator.h b/plugins/drude/openmmapi/include/openmm/DrudeNoseHooverIntegrator.h
index 087eef7136..475b3378fc 100644
--- a/plugins/drude/openmmapi/include/openmm/DrudeNoseHooverIntegrator.h
+++ b/plugins/drude/openmmapi/include/openmm/DrudeNoseHooverIntegrator.h
@@ -45,6 +45,10 @@ namespace OpenMM {
  * A second thermostat, typically with a much lower temperature, is applied to the relative internal
  * displacement of each pair.
  *
+ * This integrator can optionally set an upper limit on how far any Drude particle is ever allowed to
+ * get from its parent particle.  This can sometimes help to improve stability.  The limit is enforced
+ * with a hard wall constraint.  By default the limit is set to 0.02 nm.
+ *
  * This Integrator requires the System to include a DrudeForce, which it uses to identify the Drude
  * particles.
  */
diff --git a/plugins/drude/openmmapi/src/DrudeLangevinIntegrator.cpp b/plugins/drude/openmmapi/src/DrudeLangevinIntegrator.cpp
index 6e48cf5327..793f65c593 100644
--- a/plugins/drude/openmmapi/src/DrudeLangevinIntegrator.cpp
+++ b/plugins/drude/openmmapi/src/DrudeLangevinIntegrator.cpp
@@ -52,7 +52,7 @@ DrudeLangevinIntegrator::DrudeLangevinIntegrator(double temperature, double fric
     setFriction(frictionCoeff);
     setDrudeTemperature(drudeTemperature);
     setDrudeFriction(drudeFrictionCoeff);
-    setMaxDrudeDistance(0);
+    setMaxDrudeDistance(0.02);
     setStepSize(stepSize);
     setConstraintTolerance(1e-5);
     setRandomNumberSeed(0);
diff --git a/plugins/drude/openmmapi/src/DrudeNoseHooverIntegrator.cpp b/plugins/drude/openmmapi/src/DrudeNoseHooverIntegrator.cpp
index 911bd504c1..6a2943a374 100644
--- a/plugins/drude/openmmapi/src/DrudeNoseHooverIntegrator.cpp
+++ b/plugins/drude/openmmapi/src/DrudeNoseHooverIntegrator.cpp
@@ -54,10 +54,8 @@ namespace OpenMM {
 DrudeNoseHooverIntegrator::DrudeNoseHooverIntegrator(double temperature, double collisionFrequency,
                                                      double drudeTemperature, double drudeCollisionFrequency,
                                                      double stepSize, int chainLength, int numMTS, int numYoshidaSuzuki) :
-    NoseHooverIntegrator(stepSize),
-    drudeTemperature(drudeTemperature)
-{
-    setMaxDrudeDistance(0);
+            NoseHooverIntegrator(stepSize), drudeTemperature(drudeTemperature) {
+    setMaxDrudeDistance(0.02);
     hasSubsystemThermostats_ = false;
     addSubsystemThermostat(std::vector<int>(), std::vector<std::pair<int, int>>(), temperature,
                            collisionFrequency, drudeTemperature, drudeCollisionFrequency,
diff --git a/plugins/drude/openmmapi/src/DrudeSCFIntegrator.cpp b/plugins/drude/openmmapi/src/DrudeSCFIntegrator.cpp
index 92cb0454ea..04c751b870 100644
--- a/plugins/drude/openmmapi/src/DrudeSCFIntegrator.cpp
+++ b/plugins/drude/openmmapi/src/DrudeSCFIntegrator.cpp
@@ -48,6 +48,7 @@ DrudeSCFIntegrator::DrudeSCFIntegrator(double stepSize) : DrudeIntegrator(stepSi
     setStepSize(stepSize);
     setMinimizationErrorTolerance(0.1);
     setConstraintTolerance(1e-5);
+    setMaxDrudeDistance(0.0);
 }

 void DrudeSCFIntegrator::initialize(ContextImpl& contextRef) {
@@ -64,6 +65,8 @@ void DrudeSCFIntegrator::initialize(ContextImpl& contextRef) {
         }
     if (force == NULL)
         throw OpenMMException("The System does not contain a DrudeForce");
+    if (getMaxDrudeDistance() != 0.0)
+        throw OpenMMException("DrudeSCFIntegrator does not currently support setting max Drude distance");
     context = &contextRef;
     owner = &contextRef.getOwner();
     kernel = context->getPlatform().createKernel(IntegrateDrudeSCFStepKernel::Name(), contextRef);

diff --git a/wrappers/python/tests/TestAPIUnits.py b/wrappers/python/tests/TestAPIUnits.py
index 6c404754c8..5522bcc452 100644
--- a/wrappers/python/tests/TestAPIUnits.py
+++ b/wrappers/python/tests/TestAPIUnits.py
@@ -1051,7 +1051,7 @@ def testDrudeLangevinIntegrator(self):
         self.assertEqual(integrator.getStepSize(), 0.1*femtosecond)
         integrator.setStepSize(0.0005)
         self.assertEqual(integrator.getStepSize(), 0.0005*picosecond)
-        self.assertEqual(integrator.getMaxDrudeDistance(), 0*nanometer)
+        self.assertEqual(integrator.getMaxDrudeDistance(), 0.02*nanometer)
         integrator.setMaxDrudeDistance(0.05)
         self.assertEqual(integrator.getMaxDrudeDistance(), 0.05*nanometer)


computeDrudeTemperature (openmm/openmm#3512)

Adds a convenience function to compute the instantaneous Drude temperature in the `DrudeNoseHooverIntegrator` and `DrudeLangevinIntegrator`.

computeDrudeTemperature

### Issue Description

#### Problem
Currently, there is no direct way to compute the instantaneous temperature of the Drude system in simulations that involve Drude particle pairs. This temperature, which reflects the kinetic energy of the internal motion of Drude pairs, is critical for monitoring and validating the behavior of simulations, particularly when using integrators designed for systems with Drude particles. Without this functionality, users must rely on indirect or manual calculations, which can be cumbersome and error-prone.

#### Expected Behavior
The software should provide a convenient method to compute the instantaneous Drude temperature directly during simulations. This value should be readily accessible and accurately reflect the internal kinetic energy of Drude pairs, allowing users to verify that the Drude temperature remains close to the prescribed value. This functionality would enhance usability and improve the ability to monitor simulation performance and stability.

## 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

diff --git a/plugins/drude/openmmapi/include/openmm/DrudeLangevinIntegrator.h b/plugins/drude/openmmapi/include/openmm/DrudeLangevinIntegrator.h
index 8f39c6e075..5b26adcfbc 100644
--- a/plugins/drude/openmmapi/include/openmm/DrudeLangevinIntegrator.h
+++ b/plugins/drude/openmmapi/include/openmm/DrudeLangevinIntegrator.h
@@ -126,6 +126,12 @@ class OPENMM_EXPORT_DRUDE DrudeLangevinIntegrator : public DrudeIntegrator {
      * getTemperature().
      */
     double computeSystemTemperature();
+    /**
+     * Compute the instantaneous temperature of the Drude system, measured in Kelvin.
+     * This is calculated based on the kinetic energy of the internal motion of Drude pairs
+     * and should remain close to the prescribed Drude temperature.
+     */
+    double computeDrudeTemperature();
 protected:
     /**
      * This will be called by the Context when it is created.  It informs the Integrator
diff --git a/plugins/drude/openmmapi/include/openmm/DrudeNoseHooverIntegrator.h b/plugins/drude/openmmapi/include/openmm/DrudeNoseHooverIntegrator.h
index 475b3378fc..7bc87a8738 100644
--- a/plugins/drude/openmmapi/include/openmm/DrudeNoseHooverIntegrator.h
+++ b/plugins/drude/openmmapi/include/openmm/DrudeNoseHooverIntegrator.h
@@ -106,6 +106,12 @@ class OPENMM_EXPORT_DRUDE DrudeNoseHooverIntegrator : public NoseHooverIntegrato
      * getTemperature().
      */
     double computeSystemTemperature();
+    /**
+     * Compute the instantaneous temperature of the Drude system, measured in Kelvin.
+     * This is calculated based on the kinetic energy of the internal motion of Drude pairs
+     * and should remain close to the prescribed Drude temperature.
+     */
+     double computeDrudeTemperature();
 protected:
     /**
      * Return a list of velocities normally distributed around a target temperature, with the Drude
diff --git a/plugins/drude/openmmapi/src/DrudeHelpers.cpp b/plugins/drude/openmmapi/src/DrudeHelpers.cpp
index da5823bf02..b3eb941cf0 100644
--- a/plugins/drude/openmmapi/src/DrudeHelpers.cpp
+++ b/plugins/drude/openmmapi/src/DrudeHelpers.cpp
@@ -127,12 +127,16 @@ vector<Vec3> assignDrudeVelocities(const System &system, double temperature, dou
     return velocities;
 }

-double computeSystemTemperatureFromVelocities(const System& system, const vector<Vec3>& velocities) {
+
+/**
+ * Computes the instantaneous temperatures of the system and the internal Drude motion and returns a pair (T_system, T_drude)
+ */
+pair<double, double> computeTemperaturesFromVelocities(const System& system, const vector<Vec3>& velocities) {
     vector<int> normalParticles;
     vector<pair<int, int> > pairParticles;
     findParticlesAndPairs(system, normalParticles, pairParticles);
-    double energy = 0.0;
-    int dof = 0;
+    double energy = 0.0, drudeEnergy = 0;
+    int dof = 0, drudeDof = 0;

     // Kinetic energy and degrees of freedom from normal particles.

@@ -151,10 +155,14 @@ double computeSystemTemperatureFromVelocities(const System& system, const vector
         int p2 = pair.second;
         double mass1 = system.getParticleMass(p1);
         double mass2 = system.getParticleMass(p2);
+	double reducedMass = (mass1*mass2)/(mass1+mass2);
         if (mass1 != 0 || mass2 != 0) {
             Vec3 momentum = mass1*velocities[p1] + mass2*velocities[p2];
             energy += momentum.dot(momentum)/(mass1+mass2);
-            dof += 3;
+	    Vec3 drudeVelocity = velocities[p1] - velocities[p2];
+            drudeEnergy += reducedMass*drudeVelocity.dot(drudeVelocity);
+	    dof += 3;
+	    drudeDof += 3;
         }
     }

@@ -176,7 +184,10 @@ double computeSystemTemperatureFromVelocities(const System& system, const vector
             break;
         }
     energy *= 0.5;
-    return 2*energy/(dof*BOLTZ);
+    drudeEnergy *= 0.5;
+    if (drudeDof == 0)
+        drudeDof = 1; // so that the drude temperature is reported as 0
+    return make_pair<double, double>(2*energy/(dof*BOLTZ), 2*drudeEnergy/(drudeDof*BOLTZ));
 }

 }
diff --git a/plugins/drude/openmmapi/src/DrudeLangevinIntegrator.cpp b/plugins/drude/openmmapi/src/DrudeLangevinIntegrator.cpp
index 793f65c593..8665fe3653 100644
--- a/plugins/drude/openmmapi/src/DrudeLangevinIntegrator.cpp
+++ b/plugins/drude/openmmapi/src/DrudeLangevinIntegrator.cpp
@@ -42,9 +42,10 @@
 using namespace OpenMM;
 using std::string;
 using std::vector;
+using std::pair;

 namespace OpenMM {
-    double computeSystemTemperatureFromVelocities(const System& system, const vector<Vec3>& velocities);
+	pair<double, double> computeTemperaturesFromVelocities(const System& system, const vector<Vec3>& velocities);
 }

 DrudeLangevinIntegrator::DrudeLangevinIntegrator(double temperature, double frictionCoeff, double drudeTemperature, double drudeFrictionCoeff, double stepSize) : DrudeIntegrator(stepSize) {
@@ -126,5 +127,15 @@ double DrudeLangevinIntegrator::computeSystemTemperature() {
     context->calcForcesAndEnergy(true, false, getIntegrationForceGroups());
     vector<Vec3> velocities;
     context->computeShiftedVelocities(getVelocityTimeOffset(), velocities);
-    return computeSystemTemperatureFromVelocities(context->getSystem(), velocities);
+    return computeTemperaturesFromVelocities(context->getSystem(), velocities).first;
 }
+
+double DrudeLangevinIntegrator::computeDrudeTemperature() {
+    if (context == NULL)
+        throw OpenMMException("This Integrator is not bound to a context!");
+    context->calcForcesAndEnergy(true, false, getIntegrationForceGroups());
+    vector<Vec3> velocities;
+    context->computeShiftedVelocities(getVelocityTimeOffset(), velocities);
+    return computeTemperaturesFromVelocities(context->getSystem(), velocities).second;
+}
+
diff --git a/plugins/drude/openmmapi/src/DrudeNoseHooverIntegrator.cpp b/plugins/drude/openmmapi/src/DrudeNoseHooverIntegrator.cpp
index 6a2943a374..feae183c70 100644
--- a/plugins/drude/openmmapi/src/DrudeNoseHooverIntegrator.cpp
+++ b/plugins/drude/openmmapi/src/DrudeNoseHooverIntegrator.cpp
@@ -30,6 +30,7 @@
  * -------------------------------------------------------------------------- */

 #include "openmm/DrudeNoseHooverIntegrator.h"
+#include "SimTKOpenMMRealType.h"
 #include "openmm/Context.h"
 #include "openmm/OpenMMException.h"
 #include "openmm/internal/ContextImpl.h"
@@ -45,12 +46,14 @@
 using namespace OpenMM;
 using std::string;
 using std::vector;
+using std::pair;

 namespace OpenMM {
     extern std::vector<Vec3> assignDrudeVelocities(const System &system, double temperature, double drudeTemperature, int randomSeed);
-    double computeSystemTemperatureFromVelocities(const System& system, const vector<Vec3>& velocities);
+    pair<double, double> computeTemperaturesFromVelocities(const System& system, const vector<Vec3>& velocities);
 }

+
 DrudeNoseHooverIntegrator::DrudeNoseHooverIntegrator(double temperature, double collisionFrequency,
                                                      double drudeTemperature, double drudeCollisionFrequency,
                                                      double stepSize, int chainLength, int numMTS, int numYoshidaSuzuki) :
@@ -151,8 +154,20 @@ double DrudeNoseHooverIntegrator::computeSystemTemperature() {
     context->calcForcesAndEnergy(true, false, getIntegrationForceGroups());
     vector<Vec3> velocities;
     context->computeShiftedVelocities(getVelocityTimeOffset(), velocities);
-    return computeSystemTemperatureFromVelocities(context->getSystem(), velocities);
+    return computeTemperaturesFromVelocities(context->getSystem(), velocities).first;
+}
+
+double DrudeNoseHooverIntegrator::computeDrudeTemperature() {
+    if (context == NULL)
+        throw OpenMMException("This Integrator is not bound to a context!");
+    double kE = computeDrudeKineticEnergy();
+    size_t num_dofs = 0;
+    for (const auto &nhc: noseHooverChains){
+       num_dofs += 3 * nhc.getThermostatedPairs().size();
+    }
+    return kE/(0.5 * num_dofs * BOLTZ);
 }
+
 std::vector<Vec3> DrudeNoseHooverIntegrator::getVelocitiesForTemperature(const System &system, double temperature,
                                                                          int randomSeedIn) const {
     return assignDrudeVelocities(system, temperature, drudeTemperature, randomSeedIn);
diff --git a/wrappers/python/src/swig_doxygen/swigInputConfig.py b/wrappers/python/src/swig_doxygen/swigInputConfig.py
index 702bd3d328..b88b1c24cd 100755
--- a/wrappers/python/src/swig_doxygen/swigInputConfig.py
+++ b/wrappers/python/src/swig_doxygen/swigInputConfig.py
@@ -226,6 +226,7 @@
 ("*", "getGlobalParameterDefaultValue") : (None, ()),
 ("*", "getPermutationMode") : (None, ()),
 ("*", "computeSystemTemperature") : ("unit.kelvin", ()),
+("*", "computeDrudeTemperature") : ("unit.kelvin", ()),
 ("LocalCoordinatesSite", "getOriginWeights") : (None, ()),
 ("LocalCoordinatesSite", "getXWeights") : (None, ()),
 ("LocalCoordinatesSite", "getYWeights") : (None, ()),

diff --git a/plugins/drude/tests/TestDrudeLangevinIntegrator.h b/plugins/drude/tests/TestDrudeLangevinIntegrator.h
index be88c24c8e..964624ee0b 100644
--- a/plugins/drude/tests/TestDrudeLangevinIntegrator.h
+++ b/plugins/drude/tests/TestDrudeLangevinIntegrator.h
@@ -84,7 +84,9 @@ void testSinglePair() {
         Vec3 velCM = vel[0]*(mass1/totalMass) + vel[1]*(mass2/totalMass);
         keCM += 0.5*totalMass*velCM.dot(velCM);
         Vec3 velInternal = vel[0]-vel[1];
-        keInternal += 0.5*reducedMass*velInternal.dot(velInternal);
+        double _keInternal = 0.5*reducedMass*velInternal.dot(velInternal);
+        keInternal += _keInternal;
+	ASSERT_EQUAL_TOL(integ.computeDrudeTemperature(), _keInternal/(3*0.5*BOLTZ), 1e-6);
         Vec3 delta = state.getPositions()[0]-state.getPositions()[1];
         double distance = sqrt(delta.dot(delta));
         ASSERT(distance <= maxDistance*(1+1e-6));
diff --git a/plugins/drude/tests/TestDrudeNoseHoover.h b/plugins/drude/tests/TestDrudeNoseHoover.h
index dba51a2258..af7a2a8d44 100644
--- a/plugins/drude/tests/TestDrudeNoseHoover.h
+++ b/plugins/drude/tests/TestDrudeNoseHoover.h
@@ -167,6 +167,7 @@ void testWaterBox() {
         double drudeKE = integ.computeDrudeKineticEnergy();
         double temp = KE/(0.5*numStandardDof*BOLTZ);
         double drudeTemp = drudeKE/(0.5*numDrudeDof*BOLTZ);
+	ASSERT_EQUAL_TOL(drudeTemp, integ.computeDrudeTemperature(), 1e-6);
         meanTemp = (i*meanTemp + temp)/(i+1);
         meanDrudeTemp = (i*meanDrudeTemp + drudeTemp)/(i+1);
         double heatBathEnergy = integ.computeHeatBathEnergy();


Use hex for large atom and residue IDs in PDB files (openmm/openmm#3522)

Fixes #3501.  This is the format used by VMD.  When an atom ID passes 99999 or a residue ID passes 9999, it switches to hex.

cc @billswope @swails

What to do about pdb files when residue index goes above 9999

I am not sure this is a bug fix or a feature I am asking for, but here goes.

Now that we are using pdb files to hold systems of protein+ligands+water+ions the number of water molecules is often greater than 9999.  Common practice is to give each water molecule a unique residue id in the pdb file, but the field is only 4 characters wide in the format.   I have seen pdb files produced by Maestro switch after residue id 9999 to a base 36 numbering system, using characters 0-9 and A-Z.  The next index after 9999 is A000 then A001 .....  This may work within the suite of Schrodinger programs but openmm chokes on this.  I noticed, however, that openmm does seem to take hexidecimal values in this field, so I can convert the Maestro pdb files to work with openmm.  The problem is that when I WRITE out a pdb file from within openmm, it does not convert to hexidecimal.  In fact, after residue id 9999, it stays in a decimal mode, but goes back  to 0, and starts over again with residue id 0, 1, etc.  This leaves multiple water molecules with the same residue id, and/or an ion and a water molecule with the same residue id.  Consequently, the pdb file that was written out by openmm can't then be read back into openmm.   There are probably a bunch of ways  to handle  this.  You could increment the chain id when you start the  sequence over again, or something like that.  Is there a way to handle this so that the pdb file openmm writes out after reading one in is consistent with what it just read?  Right now, it seems broken if you can't read in a pdb file that was written out by the same code.

## 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

diff --git a/wrappers/python/openmm/app/internal/pdbstructure.py b/wrappers/python/openmm/app/internal/pdbstructure.py
index 2e26462546..3711ee2bfb 100644
--- a/wrappers/python/openmm/app/internal/pdbstructure.py
+++ b/wrappers/python/openmm/app/internal/pdbstructure.py
@@ -164,10 +164,10 @@ def _load(self, input_stream):
             if command == "ATOM  " or command == "HETATM":
                 self._add_atom(Atom(pdb_line, self, self.extraParticleIdentifier))
             elif command == "CONECT":
-                atoms = [int(pdb_line[6:11])]
+                atoms = [_parse_atom_index(pdb_line[6:11])]
                 for pos in (11,16,21,26):
                     try:
-                        atoms.append(int(pdb_line[pos:pos+5]))
+                        atoms.append(_parse_atom_index(pdb_line[pos:pos+5]))
                     except:
                         pass
                 self._current_model.connects.append(atoms)
@@ -208,11 +208,9 @@ def _load(self, input_stream):
         self._finalize()

     def _reset_atom_numbers(self):
-        self._atom_numbers_are_hex = False
         self._next_atom_number = 1

     def _reset_residue_numbers(self):
-        self._residue_numbers_are_hex = False
         self._next_residue_number = 1

     def write(self, output_stream=sys.stdout):
@@ -726,18 +724,11 @@ def __init__(self, pdb_line, pdbstructure=None, extraParticleIdentifier='EP'):
         self.is_final_residue_in_chain = False
         # Start parsing fields from pdb line
         self.record_name = pdb_line[0:6].strip()
-        if pdbstructure is not None and pdbstructure._atom_numbers_are_hex:
-            self.serial_number = int(pdb_line[6:11], 16)
-        else:
-            try:
-                self.serial_number = int(pdb_line[6:11])
-            except:
-                try:
-                    self.serial_number = int(pdb_line[6:11], 16)
-                    pdbstructure._atom_numbers_are_hex = True
-                except:
-                    # Just give it the next number in sequence.
-                    self.serial_number = pdbstructure._next_atom_number
+        try:
+            self.serial_number = _parse_atom_index(pdb_line[6:11])
+        except:
+            # Just give it the next number in sequence.
+            self.serial_number = pdbstructure._next_atom_number
         self.name_with_spaces = pdb_line[12:16]
         alternate_location_indicator = pdb_line[16]

@@ -753,31 +744,27 @@ def __init__(self, pdb_line, pdbstructure=None, extraParticleIdentifier='EP'):
         self.residue_name = self.residue_name_with_spaces.strip()

         self.chain_id = pdb_line[21]
-        if pdbstructure is not None and pdbstructure._residue_numbers_are_hex:
-            self.residue_number = int(pdb_line[22:26], 16)
-        else:
+        try:
+            self.residue_number = int(pdb_line[22:26])
+        except:
             try:
-                self.residue_number = int(pdb_line[22:26])
+                self.residue_number = int(pdb_line[22:26], 16) - 0xA000 + 10000
             except:
-                try:
-                    self.residue_number = int(pdb_line[22:26], 16)
-                    pdbstructure._residue_numbers_are_hex = True
-                except:
-                    # When VMD runs out of hex values it starts filling the residue ID field with ****.
-                    # Look at the most recent atoms to figure out whether this is a new residue or not.
-                    if pdbstructure._current_model is None or pdbstructure._current_model._current_chain is None or pdbstructure._current_model._current_chain._current_residue is None:
-                        # This is the first residue in the model.
+                # When VMD runs out of hex values it starts filling the residue ID field with ****.
+                # Look at the most recent atoms to figure out whether this is a new residue or not.
+                if pdbstructure._current_model is None or pdbstructure._current_model._current_chain is None or pdbstructure._current_model._current_chain._current_residue is None:
+                    # This is the first residue in the model.
+                    self.residue_number = pdbstructure._next_residue_number
+                else:
+                    currentRes = pdbstructure._current_model._current_chain._current_residue
+                    if currentRes.name_with_spaces != self.residue_name_with_spaces:
+                        # The residue name has changed.
+                        self.residue_number = pdbstructure._next_residue_number
+                    elif self.name_with_spaces in currentRes.atoms_by_name:
+                        # There is already an atom with this name.
                         self.residue_number = pdbstructure._next_residue_number
                     else:
-                        currentRes = pdbstructure._current_model._current_chain._current_residue
-                        if currentRes.name_with_spaces != self.residue_name_with_spaces:
-                            # The residue name has changed.
-                            self.residue_number = pdbstructure._next_residue_number
-                        elif self.name_with_spaces in currentRes.atoms_by_name:
-                            # There is already an atom with this name.
-                            self.residue_number = pdbstructure._next_residue_number
-                        else:
-                            self.residue_number = currentRes.number
+                        self.residue_number = currentRes.number
         self.insertion_code = pdb_line[26]
         # coordinates, occupancy, and temperature factor belong in Atom.Location object
         x = float(pdb_line[30:38])
@@ -986,6 +973,13 @@ def __str__(self):
             return str(self.position)

+def _parse_atom_index(index):
+    """Parse the string containing an atom index, which might be either decimal or hex."""
+    try:
+        return int(index)
+    except:
+        return int(index, 16) - 0xA0000 + 100000
+
 # run module directly for testing
 if __name__=='__main__':
     # Test the examples in the docstrings
diff --git a/wrappers/python/openmm/app/pdbfile.py b/wrappers/python/openmm/app/pdbfile.py
index 99c41776a7..81a1cf1403 100644
--- a/wrappers/python/openmm/app/pdbfile.py
+++ b/wrappers/python/openmm/app/pdbfile.py
@@ -363,7 +363,7 @@ def writeModel(topology, positions, file=sys.stdout, modelIndex=None, keepIds=Fa
                 if keepIds and len(res.id) < 5:
                     resId = res.id
                 else:
-                    resId = "%4d" % ((resIndex+1)%10000)
+                    resId = _formatIndex(resIndex+1, 4)
                 if len(res.insertionCode) == 1:
                     resIC = res.insertionCode
                 else:
@@ -384,8 +384,8 @@ def writeModel(topology, positions, file=sys.stdout, modelIndex=None, keepIds=Fa
                     else:
                         atomName = atom.name
                     coords = positions[posIndex]
-                    line = "%s%5d %-4s %3s %s%4s%1s   %s%s%s  1.00  0.00          %2s  " % (
-                        recordName, atomIndex%100000, atomName, resName, chainName, resId, resIC, _format_83(coords[0]),
+                    line = "%s%5s %-4s %3s %s%4s%1s   %s%s%s  1.00  0.00          %2s  " % (
+                        recordName, _formatIndex(atomIndex, 5), atomName, resName, chainName, resId, resIC, _format_83(coords[0]),
                         _format_83(coords[1]), _format_83(coords[2]), symbol)
                     if len(line) != 80:
                         raise ValueError('Fixed width overflow detected')
@@ -393,7 +393,7 @@ def writeModel(topology, positions, file=sys.stdout, modelIndex=None, keepIds=Fa
                     posIndex += 1
                     atomIndex += 1
                 if resIndex == len(residues)-1:
-                    print("TER   %5d      %3s %s%4s" % (atomIndex, resName, chainName, resId), file=file)
+                    print("TER   %5s      %3s %s%4s" % (_formatIndex(atomIndex, 5), resName, chainName, resId), file=file)
                     atomIndex += 1
         if modelIndex is not None:
             print("ENDMDL", file=file)
@@ -450,11 +450,11 @@ def writeFooter(topology, file=sys.stdout):
             for index1 in sorted(atomBonds):
                 bonded = atomBonds[index1]
                 while len(bonded) > 4:
-                    print("CONECT%5d%5d%5d%5d" % (index1, bonded[0], bonded[1], bonded[2]), file=file)
+                    print("CONECT%5s%5s%5s%5s" % (_formatIndex(index1, 5), _formatIndex(bonded[0], 5), _formatIndex(bonded[1], 5), _formatIndex(bonded[2], 5)), file=file)
                     del bonded[:4]
-                line = "CONECT%5d" % index1
+                line = "CONECT%5s" % _formatIndex(index1, 5)
                 for index2 in bonded:
-                    line = "%s%5d" % (line, index2)
+                    line = "%s%5s" % (line, _formatIndex(index2, 5))
                 print(line, file=file)
         print("END", file=file)

@@ -470,3 +470,14 @@ def _format_83(f):
         return ('%8.3f' % f)[:8]
     raise ValueError('coordinate "%s" could not be represented '
                      'in a width-8 field' % f)
+
+def _formatIndex(index, places):
+    """Create a string representation of an atom or residue index.  If the value is larger than can fit
+    in the available space, switch to hex.
+    """
+    if index < 10**places:
+        format = f'%{places}d'
+        return format % index
+    format = f'%{places}X'
+    shiftedIndex = (index - 10**places + 10*16**(places-1)) % (16**places)
+    return format % shiftedIndex
\ No newline at end of file

diff --git a/wrappers/python/tests/TestPdbFile.py b/wrappers/python/tests/TestPdbFile.py
index 5da9f09a63..efa197a26c 100644
--- a/wrappers/python/tests/TestPdbFile.py
+++ b/wrappers/python/tests/TestPdbFile.py
@@ -94,6 +94,45 @@ def test_AltLocs(self):
             self.assertEqual(19, len(pdb.positions))
             self.assertEqual('ILE', list(pdb.topology.residues())[0].name)

+    def test_LargeFile(self):
+        """Write and read a file with more than 100,000 atoms"""
+        topology = Topology()
+        chain = topology.addChain('A')
+        for i in range(20000):
+            res = topology.addResidue('MOL', chain)
+            atoms = []
+            for j in range(6):
+                atoms.append(topology.addAtom(f'AT{j}', elem.carbon, res))
+            for j in range(5):
+                topology.addBond(atoms[j], atoms[j+1])
+        positions = [Vec3(0, 0, 0)]*topology.getNumAtoms()
+
+        # The model has 20,000 residues and 120,000 atoms.
+
+        output = StringIO()
+        PDBFile.writeFile(topology, positions, output)
+        input = StringIO(output.getvalue())
+        pdb = PDBFile(input)
+        output.close()
+        input.close()
+        self.assertEqual(len(positions), len(pdb.positions))
+        self.assertEqual(topology.getNumAtoms(), pdb.topology.getNumAtoms())
+        self.assertEqual(topology.getNumResidues(), pdb.topology.getNumResidues())
+        self.assertEqual(topology.getNumChains(), pdb.topology.getNumChains())
+        self.assertEqual(topology.getNumBonds(), pdb.topology.getNumBonds())
+        for atom in pdb.topology.atoms():
+            self.assertEqual(str(atom.index+1), atom.id)
+        for res in pdb.topology.residues():
+            self.assertEqual(str(res.index+1), res.id)
+
+        # Make sure the CONECT records were interpreted correctly.
+
+        bonds = set()
+        for atom1, atom2 in topology.bonds():
+            bonds.add(tuple(sorted((atom1.index, atom2.index))))
+        for atom1, atom2 in pdb.topology.bonds():
+            assert tuple(sorted((atom1.index, atom2.index))) in bonds
+
     def assertVecAlmostEqual(self, p1, p2, tol=1e-7):
         unit = p1.unit
         p1 = p1.value_in_unit(unit)


Added temperature adjustment to MC barostat for simulated tempering (openmm/openmm#3614)

The title more or less explains it all:
This pull request allows simulated tempering to be run in isobaric ensembles using Monte Carlo barostats by adjusting the temperature of the barostat.

fixes #3612

Simulated tempering in the isothermal-isobaric ensemble

Hi OpenMM-devs,

I am attempting to run simulated tempering in the isothermal-isobaric ensemble, however upon inspection of the code it appears to me it only works in the canonical ensemble or non-MC volume regulated simulations?

I was wondering, could one run with a Monte Carlo barostat by addition of the following code?
```python
# Select the initial temperature.

self.currentTemperature = 0
self.simulation.integrator.setTemperature(self.temperatures[self.currentTemperature])
for force in self.simulation.system.getForces():
    if force.getName() == 'MonteCarloBarostat':
        force.setDefaultTemperature(self.temperatures[self.currentTemperature])
```
and
```python
# Select this temperature.
self._hasMadeTransition = True
self.currentTemperature = j
self.simulation.integrator.setTemperature(self.temperatures[j])
for force in self.simulation.system.getForces():
    if force.getName() == 'MonteCarloBarostat':
        force.setDefaultTemperature(self.temperatures[self.currentTemperature])
```
Looking forward hearing from you and hoping to get even more hands-on with OpenMM in the future.

## 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

diff --git a/wrappers/python/openmm/app/simulatedtempering.py b/wrappers/python/openmm/app/simulatedtempering.py
index 57158241aa..1451b69b91 100644
--- a/wrappers/python/openmm/app/simulatedtempering.py
+++ b/wrappers/python/openmm/app/simulatedtempering.py
@@ -160,6 +160,9 @@ def __init__(self, simulation, temperatures=None, numTemperatures=None, minTempe

         self.currentTemperature = 0
         self.simulation.integrator.setTemperature(self.temperatures[self.currentTemperature])
+        for param in self.simulation.context.getParameters():
+            if 'MonteCarloTemperature' in param:
+                self.simulation.context.setParameter(param, self.temperatures[self.currentTemperature])

         # Add a reporter to the simulation which will handle the updates and reports.

@@ -230,6 +233,9 @@ def _attemptTemperatureChange(self, state):
                     self._hasMadeTransition = True
                     self.currentTemperature = j
                     self.simulation.integrator.setTemperature(self.temperatures[j])
+                    for param in self.simulation.context.getParameters():
+                        if 'MonteCarloTemperature' in param:
+                            self.simulation.context.setParameter(param, self.temperatures[self.currentTemperature])
                 if self._updateWeights:
                     # Update the weight factors.

diff --git a/wrappers/python/tests/TestSimulatedTempering.py b/wrappers/python/tests/TestSimulatedTempering.py
index 82d7c95284..716bab91b1 100644
--- a/wrappers/python/tests/TestSimulatedTempering.py
+++ b/wrappers/python/tests/TestSimulatedTempering.py
@@ -52,4 +52,49 @@ def testHarmonicOscillator(self):
             assert 0.97 < meanDist < 1.03
             meanEnergy = sum([0.5*1000*(r-1)**2 for r in d])/n
             expectedEnergy = (0.5*MOLAR_GAS_CONSTANT_R*t).value_in_unit(kilojoules_per_mole)
-            self.assertAlmostEqual(expectedEnergy, meanEnergy, delta=expectedEnergy*0.3)
\ No newline at end of file
+            self.assertAlmostEqual(expectedEnergy, meanEnergy, delta=expectedEnergy*0.3)
+
+
+    def testHarmonicOscillatorNPT(self):
+        """Test running simulated tempering on a harmonic oscillator with a Monte Carlo barostat."""
+        system = System()
+        system.addParticle(1.0)
+        system.addParticle(1.0)
+        force = HarmonicBondForce()
+        force.addBond(0, 1, 1.0, 1000.0)
+        system.addForce(force)
+        mcbarostat = MonteCarloBarostat(1*bar, 100*kelvin, 2)
+        system.addForce(mcbarostat)
+        force.setUsesPeriodicBoundaryConditions(True)
+        integrator = LangevinIntegrator(100*kelvin, 10/picosecond, 0.001*picosecond)
+        topology = Topology()
+        chain = topology.addChain()
+        residue = topology.addResidue('H2', chain)
+        topology.addAtom('H1', element.hydrogen, residue)
+        topology.addAtom('H2', element.hydrogen, residue)
+        simulation = Simulation(topology, system, integrator, Platform.getPlatformByName('Reference'))
+        simulation.context.setPositions([Vec3(0, 0, 0), Vec3(1, 0, 0)])
+
+        # Check the temperature is correct before creating the simulated tempering simulation
+
+        self.assertEqual(100*kelvin, integrator.getTemperature())
+        self.assertEqual(100*kelvin, simulation.context.getParameter('MonteCarloTemperature')*kelvin)
+
+        st = SimulatedTempering(simulation, numTemperatures=10, minTemperature=200*kelvin, maxTemperature=400*kelvin, tempChangeInterval=4, reportInterval=10000)
+
+        # Check the temperatures of the integrator and barostat are set to the value of minTemperature
+
+        self.assertEqual(10, len(st.temperatures))
+        self.assertEqual(200*kelvin, st.temperatures[0])
+        self.assertEqual(400*kelvin, st.temperatures[-1])
+        self.assertEqual(200*kelvin, integrator.getTemperature())
+        self.assertEqual(200*kelvin, simulation.context.getParameter('MonteCarloTemperature')*kelvin)
+
+        # Let the simulation run and assert at every step T(mcbarostat) == T(integrator) == T(tempering)
+
+        for i in range(1000):
+            st.step(2)
+            self.assertEqual(st.temperatures[st.currentTemperature], integrator.getTemperature())
+            self.assertEqual(st.temperatures[st.currentTemperature], simulation.context.getParameter('MonteCarloTemperature')*kelvin)
+
+