Repository: openmm/openmm — pull request #4168 (cpp)
Title: Fix "off by one" error in xtc time reporting.

Fixes #4163.
Both the reporter and the tests expected the first step to have index 1, causing the time of the first step to be assigned the time `dt` instead of 0.

Now running this snippet:
```python
from openmm.app import *
from openmm import *
from openmm.unit import *
from sys import stdout

pdb = PDBFile('villin.pdb')

forcefield = ForceField('amber14-all.xml', 'amber14/tip3pfb.xml')
system = forcefield.createSystem(pdb.topology, nonbondedMethod=PME,
        nonbondedCutoff=1*nanometer, constraints=HBonds)
integrator = LangevinMiddleIntegrator(300*kelvin, 1/picosecond, 0.002*picoseconds)
simulation = Simulation(pdb.topology, system, integrator)
simulation.context.setPositions(pdb.positions)
simulation.minimizeEnergy(maxIterations=100)
simulation.reporters.append(XTCReporter('traj.xtc', 1000, enforcePeriodicBox=False))
simulation.reporters.append(StateDataReporter(stdout, 1000, step=True, time=True,
        potentialEnergy=True, temperature=True))
simulation.step(10000)
```

Produces a traj file such that:
```shell
$ gmx check -f traj.xtc
                :-) GROMACS - gmx check, 2023.1-conda_forge (-:

Executable:   /home/raul/mambaforge/envs/openmm/bin.AVX2_256/gmx
Data prefix:  /home/raul/mambaforge/envs/openmm
Working dir:  /home/raul/openmm/tests
Command line:
  gmx check -f traj.xtc

Checking file traj.xtc
Reading frame       0 time 2000.000   
# Atoms  8867
Precision 0.001 (nm)
Last frame          9 time 20000.000   


Item        #frames Timestep (ps)
Step            10    2000
Time            10    2000
Lambda           0
Coords          10    2000
Velocities       0
Forces           0
Box             10    2000

GROMACS reminds you: "People are DNA's way of making more DNA." (Edward O. Wilson)
```

@sef43, in your report you mentioned an issue with just the frame time. In addition to shifting the time, this PR also reduces by one the step count, so that the first frame written has index "0" (whereas before it was 1). Is this also the desired behavior here?

Resolved issues:
title: off by one error in XTCReporter time record
body: The new XTC reporter has an off by one error in the time it records for each frame.
Issue first noticed here: https://www.ovito.org/forum/topic/dcd-format-trajectory-reader/ (login required)

It can be seen with the following script using the villin.pdb file from the cookbook:
```python
from openmm.app import *
from openmm import *
from openmm.unit import *
from sys import stdout

pdb = PDBFile('villin.pdb')

forcefield = ForceField('amber14-all.xml', 'amber14/tip3pfb.xml')
system = forcefield.createSystem(pdb.topology, nonbondedMethod=PME,
        nonbondedCutoff=1*nanometer, constraints=HBonds)
integrator = LangevinMiddleIntegrator(300*kelvin, 1/picosecond, 0.002*picoseconds)
simulation = Simulation(pdb.topology, system, integrator)
simulation.context.setPositions(pdb.positions)
simulation.minimizeEnergy(maxIterations=100)
simulation.reporters.append(XTCReporter('traj.xtc', 1000, enforcePeriodicBox=False))
simulation.reporters.append(StateDataReporter(stdout, 1000, step=True, time=True,
        potentialEnergy=True, temperature=True))
simulation.step(10000)
```

If you run `gmx check -f traj.xtc` it outputs:
```
                :-) GROMACS - gmx check, 2023.1-conda_forge (-:

Executable:   /usr/local/bin.AVX2_256/gmx
Data prefix:  /usr/local
Working dir:  /content
Command line:
  gmx check -f traj.xtc

Checking file traj.xtc
Reading frame       0 time 4000.000   
# Atoms  8867
Precision 0.001 (nm)
Last frame          9 time 22000.000   


Item        #frames Timestep (ps)
Step            10    2000
Time            10    2000
Lambda           0
Coords          10    2000
Velocities       0
Forces           0
Box             10    2000
```
Frame 0 should have time 2,000 not 4,000
and frame 9 should have time 20,000 not 22,000

## 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/xtcfile.py b/wrappers/python/openmm/app/xtcfile.py
index ab5ff5b93a..1cdd6d7e63 100644
--- a/wrappers/python/openmm/app/xtcfile.py
+++ b/wrappers/python/openmm/app/xtcfile.py
@@ -141,7 +141,7 @@ def writeModel(self, positions, unitCellDimensions=None, periodicBoxVectors=None
             )
         else:
             boxVectors = np.zeros((3, 3)).astype(np.float32)
-        step = self._modelCount * self._interval + self._firstStep
+        step = (self._modelCount - 1) * self._interval + self._firstStep
         time = step * self._dt
         xtc_write_frame(
             self._filename.encode("utf-8"),


Test patch:
diff --git a/wrappers/python/tests/TestXtcFile.py b/wrappers/python/tests/TestXtcFile.py
index c33c65d047..1b84104205 100644
--- a/wrappers/python/tests/TestXtcFile.py
+++ b/wrappers/python/tests/TestXtcFile.py
@@ -49,9 +49,9 @@ def test_xtc_triclinic(self):
             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(1, nframes + 1) * 0.001, atol=1e-5)
+                np.allclose(time, np.arange(0, nframes) * 0.001, atol=1e-5)
             )
-            self.assertTrue(np.allclose(step, np.arange(1, nframes + 1), atol=1e-5))
+            self.assertTrue(np.allclose(step, np.arange(0, nframes), atol=1e-5))
 
     def test_xtc_cubic(self):
         """Test the XTC file by writing a trajectory and reading it back. Using a cubic box"""
@@ -87,9 +87,9 @@ def test_xtc_cubic(self):
             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(1, nframes + 1) * 0.001, atol=1e-5)
+                np.allclose(time, np.arange(0, nframes) * 0.001, atol=1e-5)
             )
-            self.assertTrue(np.allclose(step, np.arange(1, nframes + 1), atol=1e-5))
+            self.assertTrue(np.allclose(step, np.arange(0, nframes), atol=1e-5))
 
     def test_xtc_box_from_topology(self):
         """Test the XTC file by writing a trajectory and reading it back. Letting the box be set from the topology"""
@@ -131,9 +131,9 @@ def test_xtc_box_from_topology(self):
             box = np.array(np.tile(boxVectors, (nframes, 1, 1))).transpose(1, 2, 0)
             self.assertTrue(np.allclose(box_read, box, atol=1e-3))
             self.assertTrue(
-                np.allclose(time, np.arange(1, nframes + 1) * 0.001, atol=1e-5)
+                np.allclose(time, np.arange(0, nframes) * 0.001, atol=1e-5)
             )
-            self.assertTrue(np.allclose(step, np.arange(1, nframes + 1), 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."""