Repository: openmm/openmm — pull request #3886 (cpp)
Title: Minimizer does not move constrained massless particles

Fixes #3884.  This addresses an obscure situation.  Normally we don't allow a constraint to involve a massless particle.  But if both particles connected by a constraint are massless we do allow it, since the constraint will just be ignored.  That makes it easy to immobilize whole molecules without worrying about what constraints they contain.  In that situation, the minimizer was incorrectly enforcing the constraint rather than ignoring it.

Resolved issues:
title: Minimization with frozen particles
body: Hi everyone,

Recently I have been looking into how one can minimize systems containing frozen particles. In the past, I have attempted to set the mass to zero for the particles that are to be frozen, which works during the normal integration:
```python
for atom in protein_atoms:
    system.setParticleMass(atom.index, 0.000*u.dalton)
integrator = mm.LangevinMiddleIntegrator(298.15*u.kelvin, 1.0/u.picoseconds, 2.0*u.femtoseconds)
sim = app.Simulation(pdb.topology, system, integrator, platform, properties)
sim.context.setPositions(pdb.positions)
sim.minimizeEnergy()
```
However, it is my impression that all particles are still being minimized using this approach. Instead, I was thinking if I should use the [setIntegrationForceGroups](http://docs.openmm.org/latest/api-python/generated/openmm.openmm.LangevinMiddleIntegrator.html#openmm.openmm.LangevinMiddleIntegrator.setIntegrationForceGroups) method from the integrator. When calling [getIntegrationForceGroups](http://docs.openmm.org/latest/api-python/generated/openmm.openmm.LangevinMiddleIntegrator.html#openmm.openmm.LangevinMiddleIntegrator.getIntegrationForceGroups)(self) it returns -1, which I assume means that the integrator is acting on all particles? 

To summarize my questions:
a. Should the ForceGroups not change for the integrator when the system contains particles with zero mass?
b. Could you provide a minimal example of how to freeze a set of particles during minimization? If done by changing the ForceGroups please show one can use either set or int as explained in the [documentation](http://docs.openmm.org/latest/api-python/generated/openmm.openmm.LangevinMiddleIntegrator.html#openmm.openmm.LangevinMiddleIntegrator.setIntegrationForceGroups).



## IMPORTANT INSTRUCTIONS

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

Hints:

Fix patch:
diff --git a/openmmapi/src/LocalEnergyMinimizer.cpp b/openmmapi/src/LocalEnergyMinimizer.cpp
index 503f45a3d0..4fba9540a9 100644
--- a/openmmapi/src/LocalEnergyMinimizer.cpp
+++ b/openmmapi/src/LocalEnergyMinimizer.cpp
@@ -137,12 +137,16 @@ static lbfgsfloatval_t evaluate(void *instance, const lbfgsfloatval_t *x, lbfgsf
         double dr = r-distance;
         double kdr = k*dr;
         energy += 0.5*kdr*dr;
-        g[3*particle1] -= kdr*delta[0];
-        g[3*particle1+1] -= kdr*delta[1];
-        g[3*particle1+2] -= kdr*delta[2];
-        g[3*particle2] += kdr*delta[0];
-        g[3*particle2+1] += kdr*delta[1];
-        g[3*particle2+2] += kdr*delta[2];
+        if (system.getParticleMass(particle1) != 0) {
+            g[3*particle1] -= kdr*delta[0];
+            g[3*particle1+1] -= kdr*delta[1];
+            g[3*particle1+2] -= kdr*delta[2];
+        }
+        if (system.getParticleMass(particle2) != 0) {
+            g[3*particle2] += kdr*delta[0];
+            g[3*particle2+1] += kdr*delta[1];
+            g[3*particle2+2] += kdr*delta[2];
+        }
     }
     return energy;
 }


Test patch:
diff --git a/tests/TestLocalEnergyMinimizer.h b/tests/TestLocalEnergyMinimizer.h
index 8fb3bbba3e..f28fb57e0f 100644
--- a/tests/TestLocalEnergyMinimizer.h
+++ b/tests/TestLocalEnergyMinimizer.h
@@ -259,6 +259,42 @@ void testForceGroups() {
     ASSERT_EQUAL_TOL(2.0, sqrt(delta.dot(delta)), 1e-4);
 }
 
+void testMasslessParticles() {
+    // Create a system with massless particles, some of which are involved in constraints.
+    
+    const int numParticles = 10;
+    System system;
+    HarmonicBondForce* force = new HarmonicBondForce();
+    system.addForce(force);
+    vector<Vec3> positions(numParticles);
+    OpenMM_SFMT::SFMT sfmt;
+    init_gen_rand(0, sfmt);
+    for (int i = 0; i < numParticles; i++) {
+        system.addParticle(i < 3 || i == 5 ? 0.0 : 1.0);
+        positions[i] = Vec3(i, 0.1*genrand_real2(sfmt), 0.1*genrand_real2(sfmt));
+    }
+    for (int i = 0; i < numParticles-1; i++) {
+        if (i < 2 || i == 6)
+            system.addConstraint(i, i+1, 1.05);
+        else
+            force->addBond(i, i+1, 1.05, 100.0);
+    }
+    
+    // Minimize it and check that massless particles have not moved, while other
+    // constraints are satisfied.
+
+    VerletIntegrator integrator(0.01);
+    Context context(system, integrator, platform);
+    context.setPositions(positions);
+    LocalEnergyMinimizer::minimize(context, 1e-5);
+    State state = context.getState(State::Positions);
+    for (int i = 0; i < numParticles; i++)
+        if (system.getParticleMass(i) == 0)
+            ASSERT_EQUAL_VEC(positions[i], state.getPositions()[i], 1e-6);
+    Vec3 delta = state.getPositions()[6]-state.getPositions()[7];
+    ASSERT_EQUAL_TOL(1.05, sqrt(delta.dot(delta)), 1e-4);
+}
+
 void runPlatformTests();
 
 int main(int argc, char* argv[]) {
@@ -269,6 +305,7 @@ int main(int argc, char* argv[]) {
         testVirtualSites();
         testLargeForces();
         testForceGroups();
+        testMasslessParticles();
         runPlatformTests();
     }
     catch(const exception& e) {