Repository: MDAnalysis/mdanalysis

Issue:
Make TPRParser also read coordinates
So a TPR file also has coordinate and velocity information inside it.  We should be able to make a Reader for this format.  This would then let `u = mda.Universe('myfile.tpr')` yield position information as well.

Because of how Universes are created, the Reader would be completely separate from the TPRParser (but could reuse components) and would have to subclass `SingleFrameReader`.  Basic example of this:

https://github.com/MDAnalysis/mdanalysis/blob/develop/package/MDAnalysis/coordinates/INPCRD.py



Discussion:
That would be nice.




Patch:
diff --git a/package/CHANGELOG b/package/CHANGELOG
index 960ec3a82c2..c389b06bc52 100644
--- a/package/CHANGELOG
+++ b/package/CHANGELOG
@@ -40,6 +40,9 @@ Fixes
    directly passing them. (Issue #3520, PR #5006)
 
 Enhancements
+ * Support has been added for reading positions and velocities
+   from GROMACS TPR files, between GROMACS version 4.x and 2025
+   (Issue #464, PR #4873)
  * Added `TemplateInferrer` and `RDKitInferrer` dataclasses to the
    `RDKitInferring` module to be used by the RDKit converter. (PR #4305)
  * Improve speed of GROMOS11 (TRC) reader (Issue #5079, PR #5080)
diff --git a/package/MDAnalysis/coordinates/TPR.py b/package/MDAnalysis/coordinates/TPR.py
new file mode 100644
index 00000000000..1940da4c924
--- /dev/null
+++ b/package/MDAnalysis/coordinates/TPR.py
@@ -0,0 +1,134 @@
+# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*-
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8
+#
+# MDAnalysis --- https://www.mdanalysis.org
+# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors
+# (see the file AUTHORS for the full list of names)
+#
+# Released under the Lesser GNU Public Licence, v2.1 or any higher version
+#
+# Please cite your use of MDAnalysis in published work:
+#
+# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler,
+# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein.
+# MDAnalysis: A Python package for the rapid analysis of molecular dynamics
+# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th
+# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy.
+# doi: 10.25080/majora-629e541a-00e
+#
+# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein.
+# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations.
+# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787
+#
+
+"""
+TPR file format --- :mod:`MDAnalysis.coordinates.TPR`
+======================================================
+
+Class for reading positions and velocities from GROMACS TPR files.
+
+
+Reading TPR files
+-----------------
+MDAnalysis can read positions and velocities from GROMACS TPR files,
+and can also do so using different versions of the topology and
+coordinate file for a given system.
+
+
+For example, both of these are supported modes for reading
+in positions and velocities from GROMACS TPR files::
+
+   >>> u = mda.Universe(TPR2020, TPR2024_4)
+   >>> u = mda.Universe(TPR2020)
+
+
+Classes
+-------
+
+.. autoclass:: TPRReader
+   :members:
+   :inherited-members:
+
+"""
+
+from . import base
+from ..lib import util
+from .timestep import Timestep
+import MDAnalysis.topology.tpr.utils as tpr_utils
+import MDAnalysis.topology.tpr.setting as S
+
+import logging
+
+logger = logging.getLogger("MDAnalysis.coordinates.TPR")
+
+import numpy as np
+
+
+class TPRReader(base.SingleFrameReaderBase):
+    # TODO: reduce duplication with `TPRparser`;
+    # we could share some state for the position
+    # in the binary file to avoid re-reading topology
+    # or perhaps combine the topology and coordinate reading
+    # with some inheritance shenanigans?
+    """Class supporting read in of positions and velocities from GROMACS TPR files.
+
+    .. versionadded:: 2.10.0
+    """
+    format = "TPR"
+    units = {"length": "nm", "velocity": "nm/ps"}
+    _Timestep = Timestep
+
+    def _read_first_frame(self):
+        # Read header/move over topology
+        # TODO: reduce duplication with TPRparser perhaps...
+        with util.openany(self.filename, mode="rb") as infile:
+            tprf = infile.read()
+        data = tpr_utils.TPXUnpacker(tprf)
+        try:
+            th = tpr_utils.read_tpxheader(data)  # tpxheader
+        except (EOFError, ValueError):
+            msg = f"{self.filename}: Invalid tpr coordinate file or cannot be recognized"
+            logger.critical(msg)
+            raise IOError(msg)
+
+        self.ts = ts = self._Timestep(th.natoms, **self._ts_kwargs)
+        self.n_atoms = th.natoms
+
+        # Starting with gromacs 2020 (tpx version 119), the body of the file
+        # is encoded differently. We change the unpacker accordingly.
+        if th.fver >= S.tpxv_AddSizeField and th.fgen >= 27:
+            actual_body_size = len(data.get_buffer()) - data.get_position()
+            if actual_body_size == 4 * th.sizeOfTprBody:
+                # See issue #2428.
+                msg = (
+                    "TPR files produced with beta versions of gromacs 2020 "
+                    "are not supported."
+                )
+                logger.critical(msg)
+                raise IOError(msg)
+            data = tpr_utils.TPXUnpacker2020.from_unpacker(data)
+
+        state_ngtc = th.ngtc  # done init_state() in src/gmxlib/tpxio.c
+        if th.bBox:
+            tpr_utils.extract_box_info(data, th.fver)
+
+        if state_ngtc > 0:
+            if th.fver < 69:  # redundancy due to  different versions
+                tpr_utils.ndo_real(data, state_ngtc)
+            tpr_utils.ndo_real(
+                data, state_ngtc
+            )  # relevant to Berendsen tcoupl_lambda
+
+        tpr_top = tpr_utils.do_mtop(
+            data, th.fver, tpr_resid_from_one=True, precision=th.precision
+        )
+
+        if th.bX:
+            self.ts._pos = np.asarray(
+                tpr_utils.ndo_rvec(data, th.natoms), dtype=np.float32
+            )
+        if th.bV:
+            self.ts.velocities = np.asarray(
+                tpr_utils.ndo_rvec(data, th.natoms), dtype=np.float32
+            )
+            self.ts.has_velocities = True
diff --git a/package/MDAnalysis/coordinates/__init__.py b/package/MDAnalysis/coordinates/__init__.py
index 602621e5ad3..a340711a414 100644
--- a/package/MDAnalysis/coordinates/__init__.py
+++ b/package/MDAnalysis/coordinates/__init__.py
@@ -182,6 +182,9 @@ class can choose an appropriate reader automatically.
    |               |           |       | velocities are processed. Module                     |
    |               |           |       | :mod:`MDAnalysis.coordinates.TRR`                    |
    +---------------+-----------+-------+------------------------------------------------------+
+   | Gromacs       | tpr       |  r    | Coordinates and velocities are read in.              |
+   |               |           |       | Module :mod:`MDAnalysis.coordinates.TPR`             |
+   +---------------+-----------+-------+------------------------------------------------------+
    | Gromacs       | tng       |  r    | Variable precision tng trajectory. Coordinates,      |
    |               |           |       | velocities and forces are processed along with any   |
    |               |           |       | `additional tng block data`_ requested for reading.  |
@@ -776,6 +779,7 @@ class can choose an appropriate reader automatically.
 from . import PDB
 from . import PDBQT
 from . import PQR
+from . import TPR
 from . import TRC
 from . import TRJ
 from . import TRR
diff --git a/package/MDAnalysis/topology/TPRParser.py b/package/MDAnalysis/topology/TPRParser.py
index 5fd29d19851..82eca33d97d 100644
--- a/package/MDAnalysis/topology/TPRParser.py
+++ b/package/MDAnalysis/topology/TPRParser.py
@@ -248,7 +248,10 @@ def parse(self, tpr_resid_from_one=True, **kwargs):
 
         if th.bTop:
             tpr_top = tpr_utils.do_mtop(
-                data, th.fver, tpr_resid_from_one=tpr_resid_from_one
+                data,
+                th.fver,
+                tpr_resid_from_one=tpr_resid_from_one,
+                precision=th.precision,
             )
         else:
             msg = f"{self.filename}: No topology found in tpr file"
diff --git a/package/MDAnalysis/topology/tpr/utils.py b/package/MDAnalysis/topology/tpr/utils.py
index a3321813921..0789fdb5f23 100644
--- a/package/MDAnalysis/topology/tpr/utils.py
+++ b/package/MDAnalysis/topology/tpr/utils.py
@@ -286,11 +286,11 @@ def extract_box_info(data, fver):
     return obj.Box(box, box_rel, box_v)
 
 
-def do_mtop(data, fver, tpr_resid_from_one=False):
+def do_mtop(data, fver, tpr_resid_from_one=False, precision=4):
     # mtop: the topology of the whole system
     symtab = do_symtab(data)
     do_symstr(data, symtab)  # system_name
-    do_ffparams(data, fver)  # params
+    ff_params = do_ffparams(data, fver)  # params
 
     nmoltype = data.unpack_int()
     moltypes = []  # non-gromacs
@@ -423,6 +423,80 @@ def do_mtop(data, fver, tpr_resid_from_one=False):
         elements = Elements(np.array(elements, dtype=object))
         top.add_TopologyAttr(elements)
 
+    # NOTE: the tpr striding code below serves the
+    # purpose of placing us in a suitable "seek" position
+    # in the binary file such that we are well placed
+    # for reading coords and velocities if they are present
+    # the order of operations is based on an analysis of
+    # the C++ code in do_mtop() function in the GROMACS
+    # source at:
+    # src/gromacs/fileio/tpxio.cpp
+    # TODO: expand tpx version support for striding to
+    # the coordinates
+    atnr = ff_params.atnr
+    if fver >= 58:
+        # TODO: the following value is important, and not sure
+        # how to access programmatically yet...
+        # from GMX source code:
+        # api/legacy/include/gromacs/topology/topology_enums.h
+        # worst case scenario we hard code it based on
+        # tpx/GMX version?
+        # for details, see gh-4873
+        SimulationAtomGroupType_size = 10
+        n_atoms = data.unpack_int()
+        if fver < 116:
+            for i in range(3 * atnr * int(precision / 4)):
+                data.unpack_int()
+        if fver < 129:
+            # NOTE: speculative, Tyler did this by
+            # inspecting binary data and relative file offsets
+            # for older tpr files relative to more recent;
+            # the stride skip appears related to `atnr` in GMX source, the
+            # number of non-bonded atom types
+            for i in range(atnr + 1):
+                data.unpack_int()
+
+        if 58 < fver <= 83:
+            for i in range(atnr * int(precision / 4) * 2):
+                data.unpack_int()
+        if fver > 83:
+            interm = data.unpack_uchar()
+        if 83 < fver < 116:
+            for i in range(2 * atnr):
+                data.unpack_int()
+        if fver > 58:
+            ngrid = data.unpack_int()
+            grid_spacing = data.unpack_int()
+            n_elements = grid_spacing ** 2
+            for i in range(ngrid):
+                for j in range(n_elements):
+                    ndo_real(data, 4)
+        for i in range(SimulationAtomGroupType_size):
+            group_size = data.unpack_int()
+            ndo_int(data, group_size)
+        n_group_names = data.unpack_int()
+        for i in range(n_group_names):
+            data.unpack_int()
+        for i in range(SimulationAtomGroupType_size):
+            n_grp_numbers = data.unpack_int()
+            if n_grp_numbers != 0:
+                for i in range(n_grp_numbers):
+                    data.unpack_uchar()
+        if fver > 119:
+            im_excl_grp_size = data.unpack_int()
+            ndo_int(data, im_excl_grp_size)
+            # TODO: why is this needed?
+            # NOTE: this `mystery_n` value is at least sometimes used
+            # for tpx >= 137, but is often 0 otherwise
+            mystery_n = data.unpack_int()
+        if fver >= 137:
+            # NOTE: Tyler observed that the first float32
+            # positional coordinate was offset by `mystery_n`
+            # 32-bit units in a sample tpx 137 tpr file
+            # by empirical testing
+            for i in range(mystery_n):
+                data.unpack_int()
+
     return top
 
 
diff --git a/package/doc/sphinx/source/documentation_pages/coordinates/TPR.rst b/package/doc/sphinx/source/documentation_pages/coordinates/TPR.rst
new file mode 100644
index 00000000000..ef37e7af1e0
--- /dev/null
+++ b/package/doc/sphinx/source/documentation_pages/coordinates/TPR.rst
@@ -0,0 +1,1 @@
+.. automodule:: MDAnalysis.coordinates.TPR
diff --git a/package/doc/sphinx/source/documentation_pages/coordinates_modules.rst b/package/doc/sphinx/source/documentation_pages/coordinates_modules.rst
index af726d0f857..a58a6b44baf 100644
--- a/package/doc/sphinx/source/documentation_pages/coordinates_modules.rst
+++ b/package/doc/sphinx/source/documentation_pages/coordinates_modules.rst
@@ -36,6 +36,7 @@ provide the format in the keyword argument *format* to
    coordinates/PDBQT
    coordinates/PQR
    coordinates/TNG
+   coordinates/TPR
    coordinates/TRC
    coordinates/TRJ
    coordinates/TRR


Test patch:
diff --git a/testsuite/MDAnalysisTests/coordinates/test_tpr.py b/testsuite/MDAnalysisTests/coordinates/test_tpr.py
new file mode 100644
index 00000000000..09f1f45fcbe
--- /dev/null
+++ b/testsuite/MDAnalysisTests/coordinates/test_tpr.py
@@ -0,0 +1,478 @@
+# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*-
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8
+#
+# MDAnalysis --- https://www.mdanalysis.org
+# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors
+# (see the file AUTHORS for the full list of names)
+#
+# Released under the Lesser GNU Public Licence, v2.1 or any higher version
+#
+# Please cite your use of MDAnalysis in published work:
+#
+# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler,
+# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein.
+# MDAnalysis: A Python package for the rapid analysis of molecular dynamics
+# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th
+# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy.
+# doi: 10.25080/majora-629e541a-00e
+#
+# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein.
+# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations.
+# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787
+#
+
+from MDAnalysisTests.datafiles import (
+    TPR2024_4_bonded,
+    TPR_EXTRA_2024_4,
+    TPR2024_4,
+    TPR2024,
+    TPR2023,
+    TPR2022RC1,
+    TPR2022RC1_bonded,
+    TPR2021,
+    TPR2021Double,
+    TPR2021_bonded,
+    TPR2020,
+    TPR2020Double,
+    TPR2020_bonded,
+    TPR2019B3,
+    TPR2019B3_bonded,
+    TPR2018,
+    TPR2018_bonded,
+    TPR2016,
+    TPR2016_bonded,
+    TPR510,
+    TPR510_bonded,
+    TPR502,
+    TPR504,
+    TPR505,
+    TPR460,
+    TPR461,
+    TPR455,
+    TPR454,
+    TPR453,
+    TPR452,
+    TPR451,
+    TPR450,
+    TPR400,
+    TPR402,
+    TPR403,
+    TPR404,
+    TPR405,
+    TPR406,
+    TPR407,
+    TPR455Double,
+    TPR_xvf_2024_4,
+    TPR_NNPOT_2025_0,
+    TPR2020B2,
+    INPCRD,
+)
+import MDAnalysis as mda
+
+
+import pytest
+import numpy as np
+from numpy.testing import assert_allclose, assert_equal
+
+
+@pytest.mark.parametrize(
+    "tpr_file, exp_first_atom, exp_last_atom, exp_shape, exp_vel_first_atom, exp_vel_last_atom",
+    [
+        # this case is an alanine dipeptide
+        # with neural network potential active
+        # and nonzero velocities
+        (
+            TPR_NNPOT_2025_0,  # tpx 137
+            [2.36700e00, 2.30000e-02, 9.20000e-02],
+            [2.95100e00, 2.00000e-01, 2.41000e-01],
+            (23, 3),
+            [-4.72100e-01, -2.20900e-01, -2.42800e-01],
+            [-1.11900e-01, -3.69300e-01, -6.10000e-03],
+        ),
+        (
+            TPR2024_4_bonded,  # tpx 134
+            [4.446, 4.659, 2.384],
+            [4.446, 4.659, 2.384],
+            (14, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        # same coordinates, different shape
+        (
+            TPR_EXTRA_2024_4,  # tpx 134
+            [4.446, 4.659, 2.384],
+            [4.446, 4.659, 2.384],
+            (18, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        # different coordinates and different shape
+        (
+            TPR2024_4,  # tpx 134
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        # nonzero velocities
+        (
+            TPR_xvf_2024_4,  # tpx 134
+            [3.19900e00, 1.62970e00, 1.54480e00],
+            [3.39350e00, 3.49420e00, 3.02400e00],
+            (19385, 3),
+            [-0.20668714, 0.26678202, -0.10564042],
+            [-3.38010e-02, -3.22064e-01, -1.9863836e-01],
+        ),
+        (
+            TPR2024,  # tpx 133
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2023,  # tpx 129
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2022RC1,  # tpx 127
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2022RC1_bonded,  # tpx 127
+            [4.446, 4.659, 2.384],
+            [4.446, 4.659, 2.384],
+            (14, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2021,  # tpx 122
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2021Double,  # tpx 122
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2021_bonded,  # tpx 122
+            [4.446, 4.659, 2.384],
+            [4.446, 4.659, 2.384],
+            (14, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2020,  # tpx 119
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2020Double,  # tpx 119
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2020_bonded,  # tpx 119
+            [4.446, 4.659, 2.384],
+            [4.446, 4.659, 2.384],
+            (14, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2019B3,  # tpx 116
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2019B3_bonded,  # tpx 116
+            [4.446, 4.659, 2.384],
+            [4.446, 4.659, 2.384],
+            (14, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2018,  # tpx 112
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2018_bonded,  # tpx 112
+            [4.446, 4.659, 2.384],
+            [4.446, 4.659, 2.384],
+            (14, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2016,  # tpx 110
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR2016_bonded,  # tpx 110
+            [4.446, 4.659, 2.384],
+            [4.446, 4.659, 2.384],
+            (14, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR510,  # tpx 103
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR510_bonded,  # tpx 103
+            [4.446, 4.659, 2.384],
+            [4.446, 4.659, 2.384],
+            (14, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR502,  # tpx 100
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR504,  # tpx 100
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR505,  # tpx 100
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR460,  # tpx 83
+            [1.68200e00, 1.59800e00, 3.32200e00],
+            [6.11800e00, 3.62100e00, 5.94800e00],
+            (44_052, 3),
+            [-0.07961109, -0.21509364, -0.42340994],
+            [0.30528614, 0.42575037, 0.3387295],
+        ),
+        # same tpx version, same positions,
+        # but different velocities confirmed by
+        # gmx dump
+        (
+            TPR461,  # tpx 83
+            [1.68200e00, 1.59800e00, 3.32200e00],
+            [6.11800e00, 3.62100e00, 5.94800e00],
+            (44_052, 3),
+            [0.90468854, -0.33816168, -0.21799089],
+            [-0.22830929, -0.15923975, 0.10457583],
+        ),
+        (
+            TPR455,  # tpx 73
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR454,  # tpx 73
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR453,  # tpx 73
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR452,  # tpx 73
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR451,  # tpx 73
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR450,  # tpx 73
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR455Double,  # tpx 73
+            [1.48700e00, 4.02600e00, 7.95400e00],
+            [2.90100e00, 2.67900e00, 2.23600e00],
+            (21692, 3),
+            [0.10503311, 0.22275597, 0.002303541],
+            [0.089259855, 1.0131412, 2.0914786],
+        ),
+        (
+            TPR400,  # tpx 58
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR402,  # tpx 58
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR403,  # tpx 58
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR404,  # tpx 58
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR405,  # tpx 58
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR406,  # tpx 58
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+        (
+            TPR407,  # tpx 58
+            [3.25000e-01, 1.00400e00, 1.03800e00],
+            [-2.56000e-01, 1.37300e00, 3.59800e00],
+            (2263, 3),
+            np.zeros(3),
+            np.zeros(3),
+        ),
+    ],
+)
+@pytest.mark.parametrize("double_incantation", [True, False])
+def test_basic_read_tpr(
+    tpr_file,
+    exp_first_atom,
+    exp_last_atom,
+    exp_shape,
+    exp_vel_first_atom,
+    exp_vel_last_atom,
+    double_incantation,
+):
+    # verify basic ability to read positions and
+    # velocities from GMX .tpr files
+    # expected values are from gmx dump
+    if double_incantation:
+        u = mda.Universe(tpr_file, tpr_file)
+    else:
+        u = mda.Universe(tpr_file)
+    assert_allclose(u.atoms.positions[0, ...], exp_first_atom)
+    assert_allclose(u.atoms.positions[-1, ...], exp_last_atom)
+    assert_equal(u.atoms.positions.shape, exp_shape)
+    assert_allclose(u.atoms.velocities[0, ...], exp_vel_first_atom)
+    assert_allclose(u.atoms.velocities[-1, ...], exp_vel_last_atom)
+    assert_equal(u.atoms.velocities.shape, exp_shape)
+
+
+def test_error_handling_incorrect_format():
+    with pytest.raises(IOError, match="Invalid tpr coordinate file"):
+        u = mda.Universe(TPR2024_4_bonded, INPCRD, format="TPR")
+
+
+def test_2020B2_unsupported():
+    with pytest.raises(IOError, match="beta versions of gromacs 2020"):
+        u = mda.Universe(TPR2020, TPR2020B2)
+
+
+def test_different_versions():
+    # for now, we only have crude testing for
+    # reading topology and positions/velocities
+    # for the same system with different TPR
+    # file versions
+    exp_first_atom = [3.25000e-01, 1.00400e00, 1.03800e00]
+    exp_last_atom = [-2.56000e-01, 1.37300e00, 3.59800e00]
+    exp_shape = (2263, 3)
+    exp_vel = np.zeros(3)
+    u = mda.Universe(TPR2020, TPR2024_4)
+    assert_allclose(u.atoms.positions[0, ...], exp_first_atom)
+    assert_allclose(u.atoms.positions[-1, ...], exp_last_atom)
+    assert_allclose(u.atoms.velocities[0, ...], exp_vel)
+    assert_allclose(u.atoms.velocities[-1, ...], exp_vel)
+    assert_equal(u.atoms.positions.shape, exp_shape)
+    assert_equal(u.atoms.velocities.shape, exp_shape)
diff --git a/testsuite/MDAnalysisTests/data/cobrotoxin_2024_4.tpr b/testsuite/MDAnalysisTests/data/cobrotoxin_2024_4.tpr
new file mode 100644
index 00000000000..63925f46e6e
Binary files /dev/null and b/testsuite/MDAnalysisTests/data/cobrotoxin_2024_4.tpr differ
diff --git a/testsuite/MDAnalysisTests/datafiles.py b/testsuite/MDAnalysisTests/datafiles.py
index 879bce48913..81a2ac46487 100644
--- a/testsuite/MDAnalysisTests/datafiles.py
+++ b/testsuite/MDAnalysisTests/datafiles.py
@@ -96,6 +96,7 @@
     "PDB_xvf",
     "TPR_xvf",
     "TRR_xvf",  # Gromacs coords/veloc/forces (cobrotoxin, OPLS-AA, Gromacs 4.5.5 tpr)
+    "TPR_xvf_2024_4",
     "H5MD_xvf",  # TPR_xvf + TRR_xvf converted to h5md format
     "H5MD_energy",  # H5MD trajectory with observables/atoms/energy
     "H5MD_malformed",  # H5MD trajectory with malformed observable group
@@ -527,6 +528,7 @@
 ).as_posix()
 PDB_xvf = (_data_ref / "cobrotoxin.pdb").as_posix()
 TPR_xvf = (_data_ref / "cobrotoxin.tpr").as_posix()
+TPR_xvf_2024_4 = (_data_ref / "cobrotoxin_2024_4.tpr").as_posix()
 TRR_xvf = (_data_ref / "cobrotoxin.trr").as_posix()
 H5MD_xvf = (_data_ref / "cobrotoxin.h5md").as_posix()
 H5MD_energy = (_data_ref / "cu.h5md").as_posix()