Repository: MDAnalysis/mdanalysis (Python)

Issue:
repr of TopologyObjects should show order of underlying atom IDs
## Expected behavior ##
Representations of topology objects should list atom IDs in the order of the underlying AtomGroup.

## Actual behavior ##

I found it quite confusing when I looked at a Dihedral object and the order of atoms was different from my carefully crafted order to define the dihedral:
```python
>>> dih = u.atoms[[10, 0, 1, 3]].dihedral
>>> list(dih.atoms)
[<Atom 11: HG1 of type 3 of resname MET, resid 1 and segid 4AKE>,
 <Atom 1: N of type 56 of resname MET, resid 1 and segid 4AKE>,
 <Atom 2: HT1 of type 2 of resname MET, resid 1 and segid 4AKE>,
 <Atom 4: HT3 of type 2 of resname MET, resid 1 and segid 4AKE>]
>>> repr(dih)
'<Dihedral between: Atom 3, Atom 1, Atom 0, Atom 10>'
```

I’d much rather have the actual order output.

## Relevant code

Why do we do something special to the order of atom indices in an TopologyObject when printing the repr? Namely in https://github.com/MDAnalysis/mdanalysis/blob/4f36e2a06eb2d04274dfe4510910986c2dd17681/package/MDAnalysis/core/topologyobjects.py#L118 
```python
indices = (self.indices if self.indices[0] < self.indices[-1] else self.indices[::-1])
```
why do we reverse the order?



## Code to reproduce the behavior ##

``` python
>>> import MDAnalysis as mda
>>> from MDAnalysis.tests.datafiles import PSF, DCD,  GRO, PDB, TPR, XTC, TRR,  PRMncdf, NCDF
>>> u = mda.Universe(PSF, DCD)
>>> dih = u.atoms[[10, 0, 1, 3]].dihedral
>>> list(dih.atoms)
[<Atom 11: HG1 of type 3 of resname MET, resid 1 and segid 4AKE>,
 <Atom 1: N of type 56 of resname MET, resid 1 and segid 4AKE>,
 <Atom 2: HT1 of type 2 of resname MET, resid 1 and segid 4AKE>,
 <Atom 4: HT3 of type 2 of resname MET, resid 1 and segid 4AKE>]
>>> repr(dih)
'<Dihedral between: Atom 3, Atom 1, Atom 0, Atom 10>'
```


## Current version of MDAnalysis ##

- Which version are you using? (run `python -c "import MDAnalysis as mda; print(mda.__version__)"`) 2.5.0
- Which version of Python (`python -V`)? 3.10.0
- Which operating system? macOS



Discussion:
@orbeckst the "forwards" and "backwards" of bonds/angles/torsions are equivalent, so there's a canonicalisation check to make sure that the first atom is less than the last.  Internally we often use this check to deduplicate bonds.

WRT this repr, I think I've taken this a little too far and made the repr print out the "canonical" order of the atoms, rather than the true internally stored order of the data.  Fixing it seems like a good idea
Just want to confirm if the canonicalisation check is still required at all? If not we could remove

```indices = (self.indices if self.indices[0] < self.indices[-1] else self.indices[::-1])```

and simply return the representation in the order of `self.indices`.
Yeah removing that is harmless and would solve the issue

On Sat, Jul 8, 2023 at 08:53, ztimol ***@***.***> wrote:

> Just want to confirm if the canonicalisation check is still required at
> all? If not we could remove
>
> indices = (self.indices if self.indices[0] < self.indices[-1] else
> self.indices[::-1])
>
> and simply return the representation in the order of self.indices.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/MDAnalysis/mdanalysis/issues/4181#issuecomment-1626900633>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ACGSGB2PB7IGQK7QIQSMMO3XPEGXDANCNFSM6AAAAAAZWIHAY4>
> .
> You are receiving this because you commented.Message ID:
> ***@***.***>
>




Patch:
diff --git a/package/AUTHORS b/package/AUTHORS
index 033461c17a6..3e86580e235 100644
--- a/package/AUTHORS
+++ b/package/AUTHORS
@@ -219,6 +219,7 @@ Chronological list of authors
   - Daniel J. Evans
   - Mohit Kumar
   - Shubham Kumar
+  - Zaheer Timol
 
 External code
 -------------
diff --git a/package/CHANGELOG b/package/CHANGELOG
index 0b1266e7e7f..87bcbd87531 100644
--- a/package/CHANGELOG
+++ b/package/CHANGELOG
@@ -13,7 +13,8 @@ The rules for this file:
   * release numbers follow "Semantic Versioning" http://semver.org
 
 ------------------------------------------------------------------------------
-??/??/?? IAlibay, jaclark5, MohitKumar020291, orionarcher, xhgchen
+
+??/??/?? IAlibay, jaclark5, MohitKumar020291, orionarcher, xhgchen, ztimol
 
  * 2.6.0
 
@@ -29,6 +30,9 @@ Enhancements
     (Issue #3546)
 
 Changes
+  * Atom ID representation order updated to match that of associated
+    AtomGroup indices.
+    (PR #4191, Issue #4181)
   * Package license has been updated to GPLv3+ to better reflect the
     compatibility of GPLv2+ with Apache and GPLv3 licenced codes (PR #4219).
   * All new contributions are now made under LGPLv2.1+ (PR #4219).
diff --git a/package/MDAnalysis/core/topologyobjects.py b/package/MDAnalysis/core/topologyobjects.py
index c620902d81d..40e420f5df8 100644
--- a/package/MDAnalysis/core/topologyobjects.py
+++ b/package/MDAnalysis/core/topologyobjects.py
@@ -52,6 +52,8 @@ class TopologyObject(object):
        via the ``is_guessed`` managed property.
     .. versionadded:: 0.11.0
        Added the `value` method to return the size of the object
+    .. versionchanged:: 2.6.0
+       Updated Atom ID representation order to match that of AtomGroup indices
     """
     __slots__ = ("_ix", "_u", "btype", "_bondtype", "_guessed", "order")
 
@@ -116,13 +118,12 @@ def __hash__(self):
         return hash((self._u, tuple(self.indices)))
 
     def __repr__(self):
-        indices = (self.indices if self.indices[0] < self.indices[-1]
-                   else self.indices[::-1])
+        """Return representation in same order of AtomGroup indices"""
         return "<{cname} between: {conts}>".format(
             cname=self.__class__.__name__,
             conts=", ".join([
                 "Atom {0}".format(i)
-                for i in indices]))
+                for i in self.indices]))
 
     def __contains__(self, other):
         """Check whether an atom is in this :class:`TopologyObject`"""


Test patch:
diff --git a/testsuite/MDAnalysisTests/core/test_topologyobjects.py b/testsuite/MDAnalysisTests/core/test_topologyobjects.py
index 380a73b8afe..ac5f0353386 100644
--- a/testsuite/MDAnalysisTests/core/test_topologyobjects.py
+++ b/testsuite/MDAnalysisTests/core/test_topologyobjects.py
@@ -157,7 +157,7 @@ def test_angle(self, PSFDCD):
     def test_angle_repr(self, PSFDCD):
         angle = PSFDCD.atoms[[30, 10, 20]].angle
 
-        assert_equal(repr(angle), '<Angle between: Atom 20, Atom 10, Atom 30>')
+        assert_equal(repr(angle), '<Angle between: Atom 30, Atom 10, Atom 20>')
 
     def test_angle_180(self):
         # we edit the coordinates, so make our own universe
@@ -183,7 +183,7 @@ def test_dihedral_repr(self, PSFDCD):
         dihedral = PSFDCD.atoms[[4, 7, 8, 1]].dihedral
 
         assert_equal(repr(dihedral),
-                     '<Dihedral between: Atom 1, Atom 8, Atom 7, Atom 4>')
+                     '<Dihedral between: Atom 4, Atom 7, Atom 8, Atom 1>')
 
     # Improper_Dihedral class check
     def test_improper(self, PSFDCD):
@@ -197,12 +197,12 @@ def test_improper_repr(self, PSFDCD):
 
         assert_equal(
             repr(imp),
-            '<ImproperDihedral between: Atom 1, Atom 8, Atom 7, Atom 4>')
+            '<ImproperDihedral between: Atom 4, Atom 7, Atom 8, Atom 1>')
 
     def test_ureybradley_repr(self, PSFDCD):
         ub = PSFDCD.atoms[[30, 10]].ureybradley
 
-        assert_equal(repr(ub), '<UreyBradley between: Atom 10, Atom 30>')
+        assert_equal(repr(ub), '<UreyBradley between: Atom 30, Atom 10>')
 
     def test_ureybradley_repr_VE(self, PSFDCD):
         with pytest.raises(ValueError):
@@ -221,7 +221,7 @@ def test_cmap_repr(self, PSFDCD):
 
         assert_equal(
             repr(cmap),
-            '<CMap between: Atom 2, Atom 1, Atom 8, Atom 7, Atom 4>')
+            '<CMap between: Atom 4, Atom 7, Atom 8, Atom 1, Atom 2>')
     
     def test_cmap_repr_VE(self, PSFDCD):
         with pytest.raises(ValueError):