From b90e7e875de2ee4e9f6bec1c7df5e0482fb96586 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Fri, 12 Dec 2025 14:23:06 -0800 Subject: [PATCH 1/8] add ligandatommapper --- gufe/mapping/__init__.py | 2 +- gufe/mapping/atom_mapper.py | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/gufe/mapping/__init__.py b/gufe/mapping/__init__.py index 29a9bfd2f..64bda98da 100644 --- a/gufe/mapping/__init__.py +++ b/gufe/mapping/__init__.py @@ -3,7 +3,7 @@ """Defining the relationship between different components""" from .atom_mapper import AtomMapper -from .atom_mapping import AtomMapping +from .atom_mapping import AtomMapping, LigandAtomMapping from .componentmapping import ComponentMapping from .errors import AtomMappingError from .ligandatommapping import LigandAtomMapping diff --git a/gufe/mapping/atom_mapper.py b/gufe/mapping/atom_mapper.py index c28449796..4e82c06ce 100644 --- a/gufe/mapping/atom_mapper.py +++ b/gufe/mapping/atom_mapper.py @@ -6,6 +6,7 @@ import gufe from ..tokenization import GufeTokenizable +from . import LigandAtomMapping from .atom_mapping import AtomMapping @@ -26,3 +27,60 @@ def suggest_mappings(self, A: gufe.Component, B: gufe.Component) -> Iterator[Ato atom mappings between two :class:`.Component` objects. """ ... + + +class LigandAtomMapper(gufe.AtomMapper): + """ + Suggest atom mappings between two :class:`SmallMoleculeComponent` instances. + + Subclasses will typically implement the ``_mappings_generator`` method, + which returns an iterable of :class:`.LigandAtomMapping` suggestions. + """ + + @abc.abstractmethod + def _mappings_generator( + self, + componentA: SmallMoleculeComponent, + componentB: SmallMoleculeComponent, + ) -> Iterable[dict[int, int]]: + """ + Suggest mapping options for the input molecules. + + Parameters + ---------- + componentA, componentB : rdkit.Mol + the two molecules to create a mapping for + + Returns + ------- + Iterable[dict[int, int]] : + an iterable over proposed mappings from componentA to componentB + """ + ... + + def suggest_mappings( + self, + componentA: SmallMoleculeComponent, + componentB: SmallMoleculeComponent, + ) -> Iterable[LigandAtomMapping]: + """ + Suggest :class:`.LigandAtomMapping` options for the input molecules. + + Parameters + --------- + componentA, componentB : :class:`.SmallMoleculeComponent` + the two molecules to create a mapping for + + Returns + ------- + Iterable[LigandAtomMapping] : + an iterable 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) From d47ba91ee37234a74af9033fe0e0f267e1ce112d Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Fri, 12 Dec 2025 15:03:42 -0800 Subject: [PATCH 2/8] fix imports --- gufe/mapping/__init__.py | 2 +- gufe/mapping/atom_mapper.py | 58 ----------------------------- gufe/mapping/ligandatommapper.py | 63 ++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 59 deletions(-) create mode 100644 gufe/mapping/ligandatommapper.py diff --git a/gufe/mapping/__init__.py b/gufe/mapping/__init__.py index 64bda98da..29a9bfd2f 100644 --- a/gufe/mapping/__init__.py +++ b/gufe/mapping/__init__.py @@ -3,7 +3,7 @@ """Defining the relationship between different components""" from .atom_mapper import AtomMapper -from .atom_mapping import AtomMapping, LigandAtomMapping +from .atom_mapping import AtomMapping from .componentmapping import ComponentMapping from .errors import AtomMappingError from .ligandatommapping import LigandAtomMapping diff --git a/gufe/mapping/atom_mapper.py b/gufe/mapping/atom_mapper.py index 4e82c06ce..c28449796 100644 --- a/gufe/mapping/atom_mapper.py +++ b/gufe/mapping/atom_mapper.py @@ -6,7 +6,6 @@ import gufe from ..tokenization import GufeTokenizable -from . import LigandAtomMapping from .atom_mapping import AtomMapping @@ -27,60 +26,3 @@ def suggest_mappings(self, A: gufe.Component, B: gufe.Component) -> Iterator[Ato atom mappings between two :class:`.Component` objects. """ ... - - -class LigandAtomMapper(gufe.AtomMapper): - """ - Suggest atom mappings between two :class:`SmallMoleculeComponent` instances. - - Subclasses will typically implement the ``_mappings_generator`` method, - which returns an iterable of :class:`.LigandAtomMapping` suggestions. - """ - - @abc.abstractmethod - def _mappings_generator( - self, - componentA: SmallMoleculeComponent, - componentB: SmallMoleculeComponent, - ) -> Iterable[dict[int, int]]: - """ - Suggest mapping options for the input molecules. - - Parameters - ---------- - componentA, componentB : rdkit.Mol - the two molecules to create a mapping for - - Returns - ------- - Iterable[dict[int, int]] : - an iterable over proposed mappings from componentA to componentB - """ - ... - - def suggest_mappings( - self, - componentA: SmallMoleculeComponent, - componentB: SmallMoleculeComponent, - ) -> Iterable[LigandAtomMapping]: - """ - Suggest :class:`.LigandAtomMapping` options for the input molecules. - - Parameters - --------- - componentA, componentB : :class:`.SmallMoleculeComponent` - the two molecules to create a mapping for - - Returns - ------- - Iterable[LigandAtomMapping] : - an iterable 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) diff --git a/gufe/mapping/ligandatommapper.py b/gufe/mapping/ligandatommapper.py new file mode 100644 index 000000000..1e216baf5 --- /dev/null +++ b/gufe/mapping/ligandatommapper.py @@ -0,0 +1,63 @@ +from typing import Iterable + +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 iterable of :class:`.LigandAtomMapping` suggestions. + """ + + @abc.abstractmethod + def _mappings_generator( + self, + componentA: SmallMoleculeComponent, + componentB: SmallMoleculeComponent, + ) -> Iterable[dict[int, int]]: + """ + Suggest mapping options for the input molecules. + + Parameters + ---------- + componentA, componentB : rdkit.Mol + the two molecules to create a mapping for + + Returns + ------- + Iterable[dict[int, int]] : + an iterable over proposed mappings from componentA to componentB + """ + ... + + def suggest_mappings( + self, + componentA: SmallMoleculeComponent, + componentB: SmallMoleculeComponent, + ) -> Iterable[LigandAtomMapping]: + """ + Suggest :class:`.LigandAtomMapping` options for the input molecules. + + Parameters + --------- + componentA, componentB : :class:`.SmallMoleculeComponent` + the two molecules to create a mapping for + + Returns + ------- + Iterable[LigandAtomMapping] : + an iterable 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) From a71f4dd4a8ec4149bfbca9f7be492e15371f74e3 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Mon, 15 Dec 2025 15:52:04 -0800 Subject: [PATCH 3/8] fix types --- gufe/mapping/ligandatommapper.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/gufe/mapping/ligandatommapper.py b/gufe/mapping/ligandatommapper.py index 1e216baf5..fff53d640 100644 --- a/gufe/mapping/ligandatommapper.py +++ b/gufe/mapping/ligandatommapper.py @@ -1,4 +1,5 @@ -from typing import Iterable +import abc +from typing import Iterator from gufe import SmallMoleculeComponent @@ -11,7 +12,7 @@ class LigandAtomMapper(AtomMapper): Suggest atom mappings between two :class:`SmallMoleculeComponent` instances. Subclasses will typically implement the ``_mappings_generator`` method, - which returns an iterable of :class:`.LigandAtomMapping` suggestions. + which returns an Iterator of :class:`.LigandAtomMapping` suggestions. """ @abc.abstractmethod @@ -19,7 +20,7 @@ def _mappings_generator( self, componentA: SmallMoleculeComponent, componentB: SmallMoleculeComponent, - ) -> Iterable[dict[int, int]]: + ) -> Iterator[dict[int, int]]: """ Suggest mapping options for the input molecules. @@ -30,8 +31,8 @@ def _mappings_generator( Returns ------- - Iterable[dict[int, int]] : - an iterable over proposed mappings from componentA to componentB + Iterator[dict[int, int]] : + an Iterator over proposed mappings from componentA to componentB """ ... @@ -39,7 +40,7 @@ def suggest_mappings( self, componentA: SmallMoleculeComponent, componentB: SmallMoleculeComponent, - ) -> Iterable[LigandAtomMapping]: + ) -> Iterator[LigandAtomMapping]: """ Suggest :class:`.LigandAtomMapping` options for the input molecules. @@ -50,8 +51,8 @@ def suggest_mappings( Returns ------- - Iterable[LigandAtomMapping] : - an iterable over proposed mappings + 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 From 69e8b397d862e945da304211656f021d1bc0873a Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Tue, 16 Dec 2025 14:06:10 -0800 Subject: [PATCH 4/8] drop abc inheritance from ComponentMapping --- gufe/mapping/componentmapping.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gufe/mapping/componentmapping.py b/gufe/mapping/componentmapping.py index 8c665a0a6..bea4378d9 100644 --- a/gufe/mapping/componentmapping.py +++ b/gufe/mapping/componentmapping.py @@ -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` From 2ea213966d26aaa8dfa9f22b0d49e2cc58aceef6 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Thu, 18 Dec 2025 10:47:49 -0800 Subject: [PATCH 5/8] make typing more robust --- gufe/mapping/atom_mapping.py | 6 +++--- gufe/mapping/componentmapping.py | 16 ++++++++++------ gufe/mapping/ligandatommapping.py | 2 +- pyproject.toml | 4 +--- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/gufe/mapping/atom_mapping.py b/gufe/mapping/atom_mapping.py index 1d1e246da..f5e6b9861 100644 --- a/gufe/mapping/atom_mapping.py +++ b/gufe/mapping/atom_mapping.py @@ -9,9 +9,9 @@ from .componentmapping import ComponentMapping -class AtomMapping(ComponentMapping, abc.ABC): - _componentA: gufe.Component - _componentB: gufe.Component +class AtomMapping[T: gufe.Component](ComponentMapping): + _componentA: T + _componentB: T """A mapping between two different atom-based Components""" diff --git a/gufe/mapping/componentmapping.py b/gufe/mapping/componentmapping.py index bea4378d9..98062a2e3 100644 --- a/gufe/mapping/componentmapping.py +++ b/gufe/mapping/componentmapping.py @@ -1,9 +1,13 @@ # This code is part of gufe and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/gufe +from typing import TypeVar + import gufe from gufe.tokenization import GufeTokenizable +T = TypeVar("T", bound=gufe.Component) + class ComponentMapping(GufeTokenizable): """A relationship between two Components stating that they transform in some way @@ -11,22 +15,22 @@ class ComponentMapping(GufeTokenizable): For components that are atom-based is specialised to :class:`.AtomMapping` """ - _componentA: gufe.Component - _componentB: gufe.Component + _componentA: T + _componentB: T - def __init__(self, componentA: gufe.Component, componentB: gufe.Component): + def __init__(self, componentA: T, componentB: T): self._componentA = componentA self._componentB = componentB - def __contains__(self, item: gufe.Component): + def __contains__(self, item: T): return item == self._componentA or item == self._componentB @property - def componentA(self) -> gufe.Component: + def componentA(self) -> T: """The first Component in the mapping""" return self._componentA @property - def componentB(self) -> gufe.Component: + def componentB(self) -> T: """The second Component in the mapping""" return self._componentB diff --git a/gufe/mapping/ligandatommapping.py b/gufe/mapping/ligandatommapping.py index f5634da5b..fc4b2a9ed 100644 --- a/gufe/mapping/ligandatommapping.py +++ b/gufe/mapping/ligandatommapping.py @@ -21,7 +21,7 @@ import py3Dmol -class LigandAtomMapping(AtomMapping): +class LigandAtomMapping(AtomMapping[SmallMoleculeComponent]): """ Container for an atom mapping between two small molecule components. diff --git a/pyproject.toml b/pyproject.toml index cf1471c5a..fc72b5c77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,15 +12,13 @@ readme = "README.md" license = "MIT" license-files = [ "LICENSE" ] authors = [ { name = "The OpenFE developers", email = "openfe@omsf.io" } ] -requires-python = ">=3.10" +requires-python = ">=3.12" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", From eb01177e624a38935f063aa67155e03e7af3ef54 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Thu, 18 Dec 2025 11:35:49 -0800 Subject: [PATCH 6/8] Revert "make typing more robust" This reverts commit 2ea213966d26aaa8dfa9f22b0d49e2cc58aceef6. --- gufe/mapping/atom_mapping.py | 6 +++--- gufe/mapping/componentmapping.py | 16 ++++++---------- gufe/mapping/ligandatommapping.py | 2 +- pyproject.toml | 4 +++- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/gufe/mapping/atom_mapping.py b/gufe/mapping/atom_mapping.py index f5e6b9861..1d1e246da 100644 --- a/gufe/mapping/atom_mapping.py +++ b/gufe/mapping/atom_mapping.py @@ -9,9 +9,9 @@ from .componentmapping import ComponentMapping -class AtomMapping[T: gufe.Component](ComponentMapping): - _componentA: T - _componentB: T +class AtomMapping(ComponentMapping, abc.ABC): + _componentA: gufe.Component + _componentB: gufe.Component """A mapping between two different atom-based Components""" diff --git a/gufe/mapping/componentmapping.py b/gufe/mapping/componentmapping.py index 98062a2e3..bea4378d9 100644 --- a/gufe/mapping/componentmapping.py +++ b/gufe/mapping/componentmapping.py @@ -1,13 +1,9 @@ # This code is part of gufe and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/gufe -from typing import TypeVar - import gufe from gufe.tokenization import GufeTokenizable -T = TypeVar("T", bound=gufe.Component) - class ComponentMapping(GufeTokenizable): """A relationship between two Components stating that they transform in some way @@ -15,22 +11,22 @@ class ComponentMapping(GufeTokenizable): For components that are atom-based is specialised to :class:`.AtomMapping` """ - _componentA: T - _componentB: T + _componentA: gufe.Component + _componentB: gufe.Component - def __init__(self, componentA: T, componentB: T): + def __init__(self, componentA: gufe.Component, componentB: gufe.Component): self._componentA = componentA self._componentB = componentB - def __contains__(self, item: T): + def __contains__(self, item: gufe.Component): return item == self._componentA or item == self._componentB @property - def componentA(self) -> T: + def componentA(self) -> gufe.Component: """The first Component in the mapping""" return self._componentA @property - def componentB(self) -> T: + def componentB(self) -> gufe.Component: """The second Component in the mapping""" return self._componentB diff --git a/gufe/mapping/ligandatommapping.py b/gufe/mapping/ligandatommapping.py index fc4b2a9ed..f5634da5b 100644 --- a/gufe/mapping/ligandatommapping.py +++ b/gufe/mapping/ligandatommapping.py @@ -21,7 +21,7 @@ import py3Dmol -class LigandAtomMapping(AtomMapping[SmallMoleculeComponent]): +class LigandAtomMapping(AtomMapping): """ Container for an atom mapping between two small molecule components. diff --git a/pyproject.toml b/pyproject.toml index fc72b5c77..cf1471c5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,13 +12,15 @@ readme = "README.md" license = "MIT" license-files = [ "LICENSE" ] authors = [ { name = "The OpenFE developers", email = "openfe@omsf.io" } ] -requires-python = ">=3.12" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", From bc8f1d2c81f9d12a25e4968d07ba85e37337629e Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Thu, 18 Dec 2025 11:36:55 -0800 Subject: [PATCH 7/8] min python 3.12 for typing things --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cf1471c5a..fc72b5c77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,15 +12,13 @@ readme = "README.md" license = "MIT" license-files = [ "LICENSE" ] authors = [ { name = "The OpenFE developers", email = "openfe@omsf.io" } ] -requires-python = ">=3.10" +requires-python = ">=3.12" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", From 68a512877c72b7cbb86fc7cba52cb01e023caa40 Mon Sep 17 00:00:00 2001 From: Alyssa Travitz Date: Thu, 18 Dec 2025 11:37:57 -0800 Subject: [PATCH 8/8] add upper bound type --- gufe/mapping/atom_mapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gufe/mapping/atom_mapper.py b/gufe/mapping/atom_mapper.py index c28449796..1bda99b6b 100644 --- a/gufe/mapping/atom_mapper.py +++ b/gufe/mapping/atom_mapper.py @@ -9,7 +9,7 @@ from .atom_mapping import AtomMapping -class AtomMapper(GufeTokenizable): +class AtomMapper[_Component: gufe.Component](GufeTokenizable): """A class for manufacturing mappings Implementations of this class can require an arbitrary and non-standardised @@ -19,7 +19,7 @@ class AtomMapper(GufeTokenizable): """ @abc.abstractmethod - def suggest_mappings(self, A: gufe.Component, B: gufe.Component) -> Iterator[AtomMapping]: + def suggest_mappings(self, A: _Component, B: _Component) -> Iterator[AtomMapping]: """Suggests possible mappings between two Components Suggests zero or more :class:`.AtomMapping` objects, which are possible