Repository: OpenFreeEnergy/openfe

Issue:
Implement AtomMapping container
Should be a read only simple data class holding enough information for mapping two sets of ints (atom indices) to each other...


```python
class AtomMapping:
    AtoB : dict[int, int]
    BtoA : dict[int, int]
```

Discussion:

Patch:
diff --git a/openfe/setup/atommapping.py b/openfe/setup/atommapping.py
index c398611d..8f7ac9fe 100644
--- a/openfe/setup/atommapping.py
+++ b/openfe/setup/atommapping.py
@@ -1,8 +1,34 @@
+# This code is part of OpenFE and is licensed under the MIT license.
+# For details, see https://github.com/OpenFreeEnergy/openfe
+from dataclasses import dataclass
+from rdkit import Chem
+from typing import Dict, Union, TypeVar
+
+RDKitMol = TypeVar('RDKitMol')
+
+
+@dataclass
 class AtomMapping:
-    @classmethod
-    def from_perses(cls, perses_mapping):
-        pass
+    """Simple container with the mapping between two Molecules
 
-    def __getitem__(self, other):
-        return None
+    Attributes
+    ----------
+    mol1, mol2 : rdkit.Mol
+      the two Molecules in the mapping
+    mol1_to_mol2, mol2_to_mol1 : dict
+      maps the index of an atom in either molecule **A** or **B** to the other.
+      If this atom has no corresponding atom, None is returned.
 
+    """
+    mol1: RDKitMol
+    mol2: RDKitMol
+    mol1_to_mol2: Dict[int, int]
+
+    def __hash__(self):
+        # hash of SMILES of each molecule, and a hash of both mappings
+        return hash((Chem.MolToSmiles(self.mol1), Chem.MolToSmiles(self.mol2),
+                     tuple(self.mol1_to_mol2.items())))
+
+    @classmethod
+    def from_perses(cls, perses_mapping):
+        raise NotImplementedError()


Test patch:
diff --git a/openfe/tests/setup/test_atommapping.py b/openfe/tests/setup/test_atommapping.py
new file mode 100644
index 00000000..c5b86ef3
--- /dev/null
+++ b/openfe/tests/setup/test_atommapping.py
@@ -0,0 +1,49 @@
+from rdkit import Chem
+import pytest
+
+from openfe.setup import AtomMapping
+
+
+@pytest.fixture(scope='module')
+def simple_mapping():
+    """Disappearing oxygen on end
+
+    C C O
+
+    C C
+    """
+    molA = Chem.MolFromSmiles('CCO')
+    molB = Chem.MolFromSmiles('CC')
+
+    m = AtomMapping(molA, molB, mol1_to_mol2={0: 0, 1: 1})
+
+    return m
+
+
+@pytest.fixture(scope='module')
+def other_mapping():
+    """Disappearing middle carbon
+
+    C C O
+
+    C   C
+    """
+    molA = Chem.MolFromSmiles('CCO')
+    molB = Chem.MolFromSmiles('CC')
+
+    m = AtomMapping(molA, molB, mol1_to_mol2={0: 0, 2: 1})
+
+    return m
+
+
+def test_atommapping_usage(simple_mapping):
+    assert simple_mapping.mol1_to_mol2[1] == 1
+    assert simple_mapping.mol1_to_mol2.get(2, None) is None
+
+    with pytest.raises(KeyError):
+        simple_mapping.mol1_to_mol2[3]
+
+
+def test_atommapping_hash(simple_mapping, other_mapping):
+    # these two mappings map the same molecules, but with a different mapping
+    assert simple_mapping is not other_mapping