Repository: openmm/openmm — pull request #3769 (cpp)
Title: Clarified multiple forces with the same global parameter

Fixes #3768.  I added documentation to clarify this case, and also made the Context throw an exception if two forces define global parameters with the same name but different default values.

Resolved issues:
title: Implicit value substitution when two  `CustomExternalForce` have global parameters with the same name
body: # general description
I found that if two custom external forces have global parameters with the same name, the value of the latter one would be substitue by the first one. Also, when I tried to get the parameter value through `getGlobalParameterDefualtValue` it will not print the 'substituted' value but the original value setted by me. (I know that naming two global parameter with the same name is probably not best practice, but this behaviour makes it difficult to debug). I recommand either emphasizing this behaviour in the documentation or throwing errors when two global parameters have the same name.

# version info
```python
print(openmm.__version__)
```
7.7

# procedures to reproduce
```python
import openmm as mm
from openmm import app
from openmm import unit
mass = 40 * unit.amu
system = mm.System()
system.addParticle(mass)

external1 = mm.CustomExternalForce("k*x")
external1.addGlobalParameter("k", 1*unit.kilojoule_per_mole/unit.nanometer)
external1.addParticle(0, [])

external2 = mm.CustomExternalForce("k*y")
external2.addGlobalParameter("k", 2*unit.kilojoule_per_mole/unit.nanometer)
external2.addParticle(0, [])
system.addForce(external1)
system.addForce(external2)
```

When I query through the force object, the paramter values are correct
```python
for i in range(system.getNumForces()):
    f = system.getForce(i)
    print(f" name of the force: {f.getName()}")
    print(f" energy function of the force: {f.getEnergyFunction()}")
    for j in range(f.getNumGlobalParameters()):
        print(f"{f.getGlobalParameterName(j)}: {f.getGlobalParameterDefaultValue(j)}")

for i, f in enumerate(system.getForces()):
    f.setForceGroup(i)
```
name of the force: CustomExternalForce
energy function of the force: k*x
k: 1.0
name of the force: CustomExternalForce
energy function of the force: k*y
k: 2.0

But when i initialize and calculate the potential energy, it's wrong
```python
topology = app.Topology()
element = app.Element.getBySymbol('Ar')
chain = topology.addChain('A')
residue = topology.addResidue('OSC', chain)
topology.addAtom('Ar', element, residue)
integrator = mm.VerletIntegrator(2*unit.femtosecond)
simulation = app.Simulation(topology, system, integrator)
simulation.context.setPositions(np.array([[1, 2, 0]])*unit.nanometer)
for i, f in enumerate(system.getForces()):
    state = simulation.context.getState(getEnergy=True, groups={i})
    print(f.getName(), state.getPotentialEnergy())
```
CustomExternalForce 1.0 kJ/mol
CustomExternalForce 2.0 kJ/mol
Where the second customexternalforce should have 2*2=4kJ/mol as potential energy

## 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/docs-source/usersguide/theory/03_custom_forces.rst b/docs-source/usersguide/theory/03_custom_forces.rst
index 17fbfdddc8..201bf9e5d9 100644
--- a/docs-source/usersguide/theory/03_custom_forces.rst
+++ b/docs-source/usersguide/theory/03_custom_forces.rst
@@ -465,6 +465,13 @@ example, if a :class:`CustomNonbondedForce` uses a long range correction, changi
 a global parameter may require the correction coefficient to be recalculated,
 which is expensive.
 
+It is possible for multiple forces to depend on the same global parameter.  To do this,
+simply have each force specify a parameter with the same name.  This can be useful
+in certain cases.  For example, in an alchemical simulation, you might have a
+parameter that interpolates between two endpoints corresponding to different molecules.
+Changing the one parameter would simultaneously modify multiple bonded and nonbonded
+forces.
+
 The other type of parameter is ones that record many values, one for each element
 of the force, such as per-particle or per-bond parameters.  These values are stored
 directly in the force object itself, and hence are part of the system definition.
diff --git a/openmmapi/src/ContextImpl.cpp b/openmmapi/src/ContextImpl.cpp
index e86b9241f6..77f3453706 100644
--- a/openmmapi/src/ContextImpl.cpp
+++ b/openmmapi/src/ContextImpl.cpp
@@ -188,6 +188,9 @@ void ContextImpl::initialize() {
     for (size_t i = 0; i < forceImpls.size(); ++i) {
         forceImpls[i]->initialize(*this);
         map<string, double> forceParameters = forceImpls[i]->getDefaultParameters();
+        for (auto param : forceParameters)
+            if (parameters.find(param.first) != parameters.end() && parameters[param.first] != forceParameters[param.first])
+                throw OpenMMException("Two Forces define different default values for the parameter '"+param.first+"'");
         parameters.insert(forceParameters.begin(), forceParameters.end());
     }
     integrator.initialize(*this);


Test patch:
diff --git a/tests/TestCustomBondForce.h b/tests/TestCustomBondForce.h
index 16f83a3d59..e63e96f8a6 100644
--- a/tests/TestCustomBondForce.h
+++ b/tests/TestCustomBondForce.h
@@ -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) 2008-2016 Stanford University and the Authors.      *
+ * Portions copyright (c) 2008-2022 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -148,6 +148,28 @@ void testIllegalVariable() {
     ASSERT(threwException);
 }
 
+void testInconsistentParameters() {
+    // Specifying two inconsistent default values for a global parameter should throw an exception.
+    
+    System system;
+    system.addParticle(1.0);
+    CustomBondForce* bonds1 = new CustomBondForce("k*r");
+    CustomBondForce* bonds2 = new CustomBondForce("k*r");
+    bonds1->addGlobalParameter("k", 1.0);
+    bonds2->addGlobalParameter("k", 2.0);
+    system.addForce(bonds1);
+    system.addForce(bonds2);
+    VerletIntegrator integrator(0.001);
+    bool threwException = false;
+    try {
+        Context context(system, integrator, platform);
+    }
+    catch (const exception& e) {
+        threwException = true;
+    }
+    ASSERT(threwException);
+}
+
 void testPeriodic() {
     // Create a force that uses periodic boundary conditions.
     
@@ -221,6 +243,7 @@ int main(int argc, char* argv[]) {
         testBonds();
         testManyParameters();
         testIllegalVariable();
+        testInconsistentParameters();
         testPeriodic();
         testEnergyParameterDerivatives();
         runPlatformTests();