Repository: openmm/openmm — pull request #4794 (cpp)
Title: Fixes precision mismatch when appending to an XTC file with 9 atoms or less

Should fix #4793.  Note that this issue in the reading code is triggered by attempting to append to an XTC because the XTC writer reads before appending to check that the number of atoms is consistent.

Resolved issues:
title: "xtc_read(): precision mismatch" when creating a new reporter appending to an existing file
body: Hi,

I encountered an error of which I could find no information anywhere else. When running the following code with some dummy systems with the same atoms, I get an error as soon as I end up on the last step. I am running a Free Energy Perturbation, using a range of different systems forming one trajectory. This error seems to be caused by using a new xtc reporter on the same file. If I use the same xtc reporter, I get the correct results.

```
with open('system1.xml') as f:
    str = f.read()
system1 = mm.XmlSerializer.deserialize(str)
with open('system2.xml') as f:
    str = f.read()
system2 = mm.XmlSerializer.deserialize(str)
pdb = mmapp.PDBFile('topology.pdb')
topology = pdb.topology
positions = pdb.positions

integrator1 = mm.VerletIntegrator(1.0*mmunit.femtoseconds)
simulation1 = mmapp.Simulation(topology, system1, integrator1)
simulation1.context.setPositions(positions)
xtcrep1 = mmapp.XTCReporter('traj.xtc',1)
simulation1.reporters.append(xtcrep1)
simulation1.step(10)
positions1 = simulation1.context.getState(getPositions=True).getPositions()

integrator2 = mm.VerletIntegrator(1.0*mmunit.femtoseconds)
simulation2 = mmapp.Simulation(topology, system2, integrator2)
simulation2.context.setPositions(positions1)
xtcrep2 = mmapp.XTCReporter('traj.xtc',1,append=True)
simulation2.reporters.append(xtcrep2)
simulation2.step(10)
```

## IMPORTANT INSTRUCTIONS

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

Hints:

Fix patch:
diff --git a/wrappers/python/openmm/app/internal/xtc_utils/src/xtc.cpp b/wrappers/python/openmm/app/internal/xtc_utils/src/xtc.cpp
index 838e3709df..de65c83fef 100644
--- a/wrappers/python/openmm/app/internal/xtc_utils/src/xtc.cpp
+++ b/wrappers/python/openmm/app/internal/xtc_utils/src/xtc.cpp
@@ -81,7 +81,9 @@ struct XTCFrame {
 
     // Read the next frame from the XTC file and store it in this object
     int readNextFrame(XDRFILE* xd) {
-        float in_prec;
+        // Preinitialize in_prec for the precision check below since it may not
+        // be modified by read_xtc if the coordinates to read are not compressed
+        float in_prec = prec;
         auto* p_ptr = reinterpret_cast<rvec*>(positions.data());
         int status = read_xtc(xd, natoms, &step, &time, box, p_ptr, &in_prec);
         if (status == exdrOK && prec != in_prec) {


Test patch:
diff --git a/wrappers/python/tests/TestXtcFile.py b/wrappers/python/tests/TestXtcFile.py
index a66c6a2a3f..dbbb93cc7a 100644
--- a/wrappers/python/tests/TestXtcFile.py
+++ b/wrappers/python/tests/TestXtcFile.py
@@ -135,6 +135,48 @@ def test_xtc_box_from_topology(self):
             )
             self.assertTrue(np.allclose(step, np.arange(0, nframes), atol=1e-5))
 
+    def test_xtc_small(self):
+        """Test the XTC file by writing a trajectory and reading it back. Using a system size below the compression threshold"""
+        with tempfile.TemporaryDirectory() as temp:
+            fname = os.path.join(temp, 'traj.xtc')
+            pdbfile = app.PDBFile("systems/ions.pdb")
+            # Set some arbitrary size for the unit cell so that a box is included in the trajectory
+            pdbfile.topology.setUnitCellDimensions([10, 10, 10])
+            natom = len(list(pdbfile.topology.atoms()))
+            nframes = 20
+            xtc = app.XTCFile(fname, pdbfile.topology, 0.001)
+            coords = []
+            box = []
+            for i in range(nframes):
+                coords.append(
+                    [mm.Vec3(random(), random(), random()) for j in range(natom)]
+                    * unit.nanometers
+                )
+                box_i = (
+                    mm.Vec3(random(), random(), random()) * unit.nanometers,
+                    mm.Vec3(random(), random(), random()) * unit.nanometers,
+                    mm.Vec3(random(), random(), random()) * unit.nanometers,
+                )
+                box.append(np.array([[vec.x, vec.y, vec.z] for vec in box_i]))
+                xtc.writeModel(coords[i], periodicBoxVectors=box_i)
+            # The  XTCFile class  does not  provide a  way to  read the
+            # trajectory back, but the underlying XTC library does
+            coords_read, box_read, time, step = read_xtc(fname.encode("utf-8"))
+            self.assertEqual(coords_read.shape, (natom, 3, nframes))
+            self.assertEqual(box_read.shape, (3, 3, nframes))
+            self.assertEqual(len(time), nframes)
+            self.assertEqual(len(step), nframes)
+            coords = np.array(
+                [c.value_in_unit(unit.nanometers) for c in coords]
+            ).transpose(1, 2, 0)
+            self.assertTrue(np.allclose(coords_read, coords, atol=1e-3))
+            box = np.array(box).transpose(1, 2, 0)
+            self.assertTrue(np.allclose(box_read, box, atol=1e-3))
+            self.assertTrue(
+                np.allclose(time, np.arange(0, nframes) * 0.001, atol=1e-5)
+            )
+            self.assertTrue(np.allclose(step, np.arange(0, nframes), atol=1e-5))
+
     def testLongTrajectory(self):
         """Test writing a trajectory that has more than 2^31 steps."""
         with tempfile.TemporaryDirectory() as temp: