Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gufe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
AtomMapper, # more specific to atom based components
AtomMapping,
ComponentMapping, # how individual Components relate
LigandAtomMapper,
LigandAtomMapping,
)
from .network import AlchemicalNetwork
Expand Down
1 change: 1 addition & 0 deletions gufe/mapping/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
from .atom_mapping import AtomMapping
from .componentmapping import ComponentMapping
from .errors import AtomMappingError
from .ligandatommapper import LigandAtomMapper
from .ligandatommapping import LigandAtomMapping
3 changes: 1 addition & 2 deletions gufe/mapping/componentmapping.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# This code is part of gufe and is licensed under the MIT license.
# For details, see https://github.com/OpenFreeEnergy/gufe
import abc

import gufe
from gufe.tokenization import GufeTokenizable


class ComponentMapping(GufeTokenizable, abc.ABC):
class ComponentMapping(GufeTokenizable):
"""A relationship between two Components stating that they transform in some way

For components that are atom-based is specialised to :class:`.AtomMapping`
Expand Down
65 changes: 65 additions & 0 deletions gufe/mapping/ligandatommapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import abc
from typing import Iterator

from gufe import SmallMoleculeComponent

from .atom_mapper import AtomMapper
from .ligandatommapping import LigandAtomMapping


class LigandAtomMapper(AtomMapper):
"""
Suggest atom mappings between two :class:`SmallMoleculeComponent` instances.

Subclasses will typically implement the ``_mappings_generator`` method,
which returns an Iterator of :class:`.LigandAtomMapping` suggestions.
"""

@abc.abstractmethod
def _mappings_generator(
self,
componentA: SmallMoleculeComponent,
componentB: SmallMoleculeComponent,
) -> Iterator[dict[int, int]]:
"""
Suggest mapping options for the input molecules.

Parameters
----------
componentA, componentB : rdkit.Mol
the two molecules to create a mapping for

Returns
-------
Iterator[dict[int, int]] :
an Iterator over proposed mappings from componentA to componentB
"""
...

def suggest_mappings(
self,
# TODO: fix overrides when we move to min python 3.12 - see https://peps.python.org/pep-0695/#summary-examples
componentA: SmallMoleculeComponent, # type: ignore[override]
componentB: SmallMoleculeComponent, # type: ignore[override]
) -> Iterator[LigandAtomMapping]:
"""
Suggest :class:`.LigandAtomMapping` options for the input molecules.

Parameters
---------
componentA, componentB : :class:`.SmallMoleculeComponent`
the two molecules to create a mapping for

Returns
-------
Iterator[LigandAtomMapping] :
an Iterator over proposed mappings
"""
# For this base class, implementation is redundant with
# _mappings_generator. However, we keep it separate so that abstract
# subclasses of this can customize suggest_mappings while always
# maintaining the consistency that concrete implementations must
# implement _mappings_generator.

for map_dct in self._mappings_generator(componentA, componentB):
yield LigandAtomMapping(componentA, componentB, map_dct)
89 changes: 89 additions & 0 deletions gufe/tests/test_atommapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import pytest
from rdkit import Chem

from gufe import LigandAtomMapper, LigandAtomMapping, SmallMoleculeComponent

from .test_tokenization import GufeTokenizableTestsMixin


def mol_from_smiles(smiles: str) -> Chem.Mol:
m = Chem.MolFromSmiles(smiles)
Chem.AllChem.Compute2DCoords(m) # type: ignore[attr-defined]

return m


@pytest.fixture(scope="session")
def simple_mapping() -> LigandAtomMapping:
"""Disappearing oxygen on end

C C O

C C
"""
molA = SmallMoleculeComponent(mol_from_smiles("CCO"))
molB = SmallMoleculeComponent(mol_from_smiles("CC"))

m = LigandAtomMapping(molA, molB, componentA_to_componentB={0: 0, 1: 1})

return m


@pytest.fixture(scope="session")
def other_mapping() -> LigandAtomMapping:
"""Disappearing middle carbon

C C O

C C
"""
molA = SmallMoleculeComponent(mol_from_smiles("CCO"))
molB = SmallMoleculeComponent(mol_from_smiles("CC"))

m = LigandAtomMapping(molA, molB, componentA_to_componentB={0: 0, 2: 1})

return m


class ExampleLigandAtomMapper(LigandAtomMapper):
def __init__(self, mappings):
self.mappings = mappings

@classmethod
def _defaults(cls):
return {}

def _to_dict(self):
return {"mappings": self.mappings}

@classmethod
def _from_dict(cls, dct):
return cls(**dct)

def _mappings_generator(self, componentA, componentB):
for mapping in self.mappings:
yield mapping.componentA_to_componentB


class TestLigandAtomMapper(GufeTokenizableTestsMixin):
cls = ExampleLigandAtomMapper
repr = None

@pytest.fixture
def instance(self, simple_mapping, other_mapping):
return ExampleLigandAtomMapper([simple_mapping, other_mapping])

def test_abstract_error(self):
# make sure users cannot use LigandAtomMapper directly
with pytest.raises(TypeError):
LigandAtomMapper()

def test_suggest_mappings(self, instance, simple_mapping, other_mapping):
# a correctly implemented ligand atom mapping should return the
# mappings generated by the _mappings_generator
molA = simple_mapping.componentA
molB = simple_mapping.componentB

results = list(instance.suggest_mappings(molA, molB))
assert len(results) == 2
assert results == [simple_mapping, other_mapping]
Loading