Repository: openmm/openmm — pull request #4641 (cpp)
Title: Serializing CustomIntegrator preserves order of global variables

Fixes #4637.

When serializing a CustomIntegrator, it recorded the global variables as attributes on a single XML tag.  That doesn't preserve order, so when you reloaded it, the global variables would come out in alphabetical order.  It now uses separate tags so the order is preserved.

Resolved issues:
title: Deepcopying `AMDIntegrator` swaps values of global parameters
body: Making a deepcopy of an `AMDIntegrator` swaps the values of the global parameters (alpha and E).
```
from openmm import AMDIntegrator
amd = AMDIntegrator(0.001, 0.0, 1.0)
amd.getAlpha()
amd.getE()
```
prints 0.0, 1.0.
```
from copy import deepcopy
amd2 = deepcopy(amd)
amd2.getAlpha()
amd2.getE()
```
prints 1.0, 0.0.

I have tried reproducing the behavior with another `CustomIntegrator`, but with no luck. Could the issue be related to the names of the global parameters?

## 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/serialization/src/CustomIntegratorProxy.cpp b/serialization/src/CustomIntegratorProxy.cpp
index b1ecf26fd2..86ab647835 100644
--- a/serialization/src/CustomIntegratorProxy.cpp
+++ b/serialization/src/CustomIntegratorProxy.cpp
@@ -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) 2010 Stanford University and the Authors.           *
+ * Portions copyright (c) 2010-2024 Stanford University and the Authors.      *
  * Authors: Peter Eastman, Yutong Zhao                                        *
  * Contributors:                                                              *
  *                                                                            *
@@ -36,23 +36,20 @@ using namespace std;
 using namespace OpenMM;
 
 CustomIntegratorProxy::CustomIntegratorProxy() : SerializationProxy("CustomIntegrator") {
-
 }
 
 void CustomIntegratorProxy::serialize(const void* object, SerializationNode& node) const {
-    node.setIntProperty("version", 2);
+    node.setIntProperty("version", 3);
     const CustomIntegrator& integrator = *reinterpret_cast<const CustomIntegrator*>(object);
     SerializationNode& globalVariablesNode = node.createChildNode("GlobalVariables");
-    for (int i = 0; i < integrator.getNumGlobalVariables(); i++) {
-        globalVariablesNode.setDoubleProperty(integrator.getGlobalVariableName(i), integrator.getGlobalVariable(i));
-    }
+    for (int i = 0; i < integrator.getNumGlobalVariables(); i++)
+        globalVariablesNode.createChildNode("Variable").setStringProperty("name", integrator.getGlobalVariableName(i)).setDoubleProperty("value", integrator.getGlobalVariable(i));
     SerializationNode& perDofVariablesNode = node.createChildNode("PerDofVariables");
     for (int i = 0; i < integrator.getNumPerDofVariables(); i++) {
         SerializationNode& perDofValuesNode = perDofVariablesNode.createChildNode(integrator.getPerDofVariableName(i));
         vector<Vec3> perDofValues; integrator.getPerDofVariable(i, perDofValues);
-        for (int j = 0; j < perDofValues.size(); j++) {
-            perDofValuesNode.createChildNode("Value").setDoubleProperty("x",perDofValues[j][0]).setDoubleProperty("y",perDofValues[j][1]).setDoubleProperty("z",perDofValues[j][2]);
-        }
+        for (int j = 0; j < perDofValues.size(); j++)
+            perDofValuesNode.createChildNode("Value").setDoubleProperty("x", perDofValues[j][0]).setDoubleProperty("y", perDofValues[j][1]).setDoubleProperty("z", perDofValues[j][2]);
     }
     SerializationNode& computationsNode = node.createChildNode("Computations");
     for (int i = 0; i < integrator.getNumComputations(); i++) {
@@ -60,26 +57,30 @@ void CustomIntegratorProxy::serialize(const void* object, SerializationNode& nod
         string computationVariable;
         string computationExpression;
         integrator.getComputationStep(i, computationType, computationVariable, computationExpression);
-        computationsNode.createChildNode("Computation").setIntProperty("computationType",static_cast<int>(computationType))
-            .setStringProperty("computationVariable",computationVariable).setStringProperty("computationExpression",computationExpression);
+        computationsNode.createChildNode("Computation").setIntProperty("computationType", static_cast<int>(computationType))
+            .setStringProperty("computationVariable", computationVariable).setStringProperty("computationExpression", computationExpression);
     }
     SerializationNode& functions = node.createChildNode("Functions");
     for (int i = 0; i < integrator.getNumTabulatedFunctions(); i++)
         functions.createChildNode("Function", &integrator.getTabulatedFunction(i)).setStringProperty("name", integrator.getTabulatedFunctionName(i));
-    node.setStringProperty("kineticEnergyExpression",integrator.getKineticEnergyExpression());
-    node.setIntProperty("randomSeed",integrator.getRandomNumberSeed());
-    node.setDoubleProperty("stepSize",integrator.getStepSize());
-    node.setDoubleProperty("constraintTolerance",integrator.getConstraintTolerance());
+    node.setStringProperty("kineticEnergyExpression", integrator.getKineticEnergyExpression());
+    node.setIntProperty("randomSeed", integrator.getRandomNumberSeed());
+    node.setDoubleProperty("stepSize", integrator.getStepSize());
+    node.setDoubleProperty("constraintTolerance", integrator.getConstraintTolerance());
 }
 
 void* CustomIntegratorProxy::deserialize(const SerializationNode& node) const {
     int version = node.getIntProperty("version");
-    if (version < 1 || version > 2)
+    if (version < 1 || version > 3)
         throw OpenMMException("Unsupported version number");
     CustomIntegrator* integrator = new CustomIntegrator(node.getDoubleProperty("stepSize"));
     const SerializationNode& globalVariablesNode = node.getChildNode("GlobalVariables");
-    for (auto& prop : globalVariablesNode.getProperties())
-        integrator->addGlobalVariable(prop.first, globalVariablesNode.getDoubleProperty(prop.first));
+    if (version < 3)
+        for (auto& prop : globalVariablesNode.getProperties())
+            integrator->addGlobalVariable(prop.first, globalVariablesNode.getDoubleProperty(prop.first));
+    else
+        for (auto& var : globalVariablesNode.getChildren())
+            integrator->addGlobalVariable(var.getStringProperty("name"), var.getDoubleProperty("value"));
     const SerializationNode& perDofVariablesNode = node.getChildNode("PerDofVariables");
     int count = 0;
     for (auto& var : perDofVariablesNode.getChildren()) {


Test patch:
diff --git a/serialization/tests/TestSerializeIntegrator.cpp b/serialization/tests/TestSerializeIntegrator.cpp
index d24fe4f74d..2663de097a 100644
--- a/serialization/tests/TestSerializeIntegrator.cpp
+++ b/serialization/tests/TestSerializeIntegrator.cpp
@@ -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) 2010-2019 Stanford University and the Authors.      *
+ * Portions copyright (c) 2010-2024 Stanford University and the Authors.      *
  * Authors: Peter Eastman, Yutong Zhao                                        *
  * Contributors:                                                              *
  *                                                                            *
@@ -154,8 +154,8 @@ void testSerializeCustomIntegrator() {
     intg->addGlobalVariable("oute", 0);
     intg->addGlobalVariable("oute1", 0);
     intg->addGlobalVariable("oute2", 0);
-    intg->addGlobalVariable("oute3_conditional_v1", 0);// HACK: need addGlobals to be alphabetical to work around bug
     intg->addGlobalVariable("oute3_conditional_v2", 0);
+    intg->addGlobalVariable("oute3_conditional_v1", 0);
     intg->addComputePerDof("outf", "f");
     intg->addComputePerDof("outf1", "f1");
     intg->addComputePerDof("outf2", "f2");