diff --git a/docs/installation.rst b/docs/installation.rst index 4d0320211..094473448 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -501,13 +501,11 @@ See :ref:`installation:mamba_hpc` for more details. Optional dependencies --------------------- -Certain functionalities are only available if you also install other, -optional packages. +Certain functionalities are only available if you also install optional packages. -* **perses tools**: To use perses, you need to install perses and OpenEye, - and you need a valid OpenEye license. To install both packages, use:: +* **OpenEye tools**: To use functionality that uses the OpenEye, you must additionally install OpenEye, and you need a valid OpenEye license. To install, use:: - $ mamba install -c openeye perses openeye-toolkits + $ mamba install -c openeye openeye-toolkits Supported Hardware ------------------ diff --git a/docs/reference/api/atom_mappers.rst b/docs/reference/api/atom_mappers.rst index b30d441f6..02398fea4 100644 --- a/docs/reference/api/atom_mappers.rst +++ b/docs/reference/api/atom_mappers.rst @@ -23,7 +23,6 @@ Tools for mapping atoms in one molecule to those in another. Used to generate ef KartografAtomMapper LomapAtomMapper - PersesAtomMapper .. rubric:: Data Types @@ -65,17 +64,3 @@ Scorers implemented by the `LOMAP `_ pa heterocycles_score transmuting_methyl_into_ring_score transmuting_ring_sizes_score - - -Perses Scorers -~~~~~~~~~~~~~~ - -Scorers implemented by the `Perses `_ package. - -.. module:: openfe.setup.atom_mapping.perses_scorers - -.. autosummary:: - :nosignatures: - :toctree: generated/ - - default_perses_scorer diff --git a/environment.yml b/environment.yml index 54a1968d1..b32011678 100644 --- a/environment.yml +++ b/environment.yml @@ -24,7 +24,6 @@ dependencies: - packaging - pandas - parmed >=4.3.1 # fix to support numpy >=2.3: https://github.com/ParmEd/ParmEd/pull/1387 - - perses>=0.10.3 - plugcli - pint>=0.24.0 - pip diff --git a/news/remove_perses.rst b/news/remove_perses.rst new file mode 100644 index 000000000..1ea8da606 --- /dev/null +++ b/news/remove_perses.rst @@ -0,0 +1,25 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* Perses atom mapper and scorer functionality is deprecated, slated to be removed in ``openfe v2.0`. + This includes ``PersesAtomMapper`` and ``default_perses_scorer`` (`PR #1857 `_). + + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/openfe/__init__.py b/src/openfe/__init__.py index a320e7e98..1610429e0 100644 --- a/src/openfe/__init__.py +++ b/src/openfe/__init__.py @@ -76,10 +76,8 @@ LigandAtomMapper, LigandNetwork, LomapAtomMapper, - PersesAtomMapper, ligand_network_planning, lomap_scorers, - perses_scorers, ) __version__ = version("openfe") diff --git a/src/openfe/setup/__init__.py b/src/openfe/setup/__init__.py index 300bc22e7..9298f20d1 100644 --- a/src/openfe/setup/__init__.py +++ b/src/openfe/setup/__init__.py @@ -7,9 +7,7 @@ LigandAtomMapper, LigandAtomMapping, LomapAtomMapper, - PersesAtomMapper, lomap_scorers, - perses_scorers, ) # TODO: circular import risk with LigandNetwork diff --git a/src/openfe/setup/atom_mapping/__init__.py b/src/openfe/setup/atom_mapping/__init__.py index e5914757a..62be898cc 100644 --- a/src/openfe/setup/atom_mapping/__init__.py +++ b/src/openfe/setup/atom_mapping/__init__.py @@ -1,7 +1,6 @@ from gufe import LigandAtomMapping from kartograf import KartografAtomMapper -from . import lomap_scorers, perses_scorers +from . import lomap_scorers from .ligandatommapper import LigandAtomMapper from .lomap_mapper import LomapAtomMapper -from .perses_mapper import PersesAtomMapper diff --git a/src/openfe/setup/atom_mapping/perses_mapper.py b/src/openfe/setup/atom_mapping/perses_mapper.py deleted file mode 100644 index b961b37cf..000000000 --- a/src/openfe/setup/atom_mapping/perses_mapper.py +++ /dev/null @@ -1,117 +0,0 @@ -# This code is part of OpenFE and is licensed under the MIT license. -# For details, see https://github.com/OpenFreeEnergy/openfe - -""" -The MCS class from Perses shamelessly wrapped and used here to match our API. - -""" - -import warnings - -from gufe.settings.typing import AngstromQuantity -from openff.units import Quantity, unit -from openff.units.openmm import to_openmm - -from openfe.utils import requires_package - -from ...utils.silence_root_logging import silence_root_logging - -try: - with silence_root_logging(): - from perses.rjmc.atom_mapping import AtomMapper, InvalidMappingException -except ImportError: - pass # Don't throw error, will happen later - -from .ligandatommapper import LigandAtomMapper - - -class PersesAtomMapper(LigandAtomMapper): - allow_ring_breaking: bool - preserve_chirality: bool - use_positions: bool - coordinate_tolerance: AngstromQuantity - - def _to_dict(self) -> dict: - # strip units but record values - return { - "allow_ring_breaking": self.allow_ring_breaking, - "preserve_chirality": self.preserve_chirality, - "use_positions": self.use_positions, - "coordinate_tolerance": self.coordinate_tolerance.m_as(unit.angstrom), - "_tolerance_unit": "angstrom", - } - - @classmethod - def _from_dict(cls, dct: dict): - # attach units again - tolerence_unit = dct.pop("_tolerance_unit") - dct["coordinate_tolerance"] *= getattr(unit, tolerence_unit) - return cls(**dct) - - @classmethod - def _defaults(cls): - return {} - - @requires_package("perses") - def __init__( - self, - allow_ring_breaking: bool = True, - preserve_chirality: bool = True, - use_positions: bool = True, - coordinate_tolerance: Quantity = 0.25 * unit.angstrom, - ): - """ - Suggest atom mappings with the Perses atom mapper. - - Parameters - ---------- - allow_ring_breaking: bool, optional - this option checks if on only full cycles of the molecules shall - be mapped, default: False - preserve_chirality: bool, optional - if mappings must strictly preserve chirality, default: True - use_positions: bool, optional - this option defines, if the - coordinate_tolerance: openff.units.unit.Quantity, optional - tolerance on how close coordinates need to be, such they - can be mapped, default: 0.25*unit.angstrom - - """ - warnings.warn( - "PersesAtomMapper is deprecated and is planned to be removed in openfe v2.0. If you have questions related to this, please open an issue at https://github.com/OpenFreeEnergy/openfe/issues.", - DeprecationWarning, - ) - self.allow_ring_breaking = allow_ring_breaking - self.preserve_chirality = preserve_chirality - self.use_positions = use_positions - self.coordinate_tolerance = coordinate_tolerance - - def _mappings_generator(self, componentA, componentB): - # Construct Perses Mapper - _atom_mapper = AtomMapper( - use_positions=self.use_positions, - coordinate_tolerance=to_openmm(self.coordinate_tolerance), - allow_ring_breaking=self.allow_ring_breaking, - ) - - # Try generating a mapping - try: - _atom_mappings = _atom_mapper.get_all_mappings( - old_mol=componentA.to_openff(), new_mol=componentB.to_openff() - ) - except InvalidMappingException: - return - - # Catch empty mappings here - if _atom_mappings is None: - return - - # Post processing - if self.preserve_chirality: - for x in _atom_mappings: - x.preserve_chirality() - - # Translate mapping objects - mapping_dict = (x.old_to_new_atom_map for x in _atom_mappings) - - yield from mapping_dict diff --git a/src/openfe/setup/atom_mapping/perses_scorers.py b/src/openfe/setup/atom_mapping/perses_scorers.py deleted file mode 100644 index be7df35ee..000000000 --- a/src/openfe/setup/atom_mapping/perses_scorers.py +++ /dev/null @@ -1,127 +0,0 @@ -# This code is part of OpenFE and is licensed under the MIT license. -# For details, see https://github.com/OpenFreeEnergy/openfe - -import warnings -from typing import Callable - -from openfe.utils import requires_package - -from ...utils.silence_root_logging import silence_root_logging - -try: - with silence_root_logging(): - from perses.rjmc.atom_mapping import AtomMapper, AtomMapping -except ImportError: - pass # Don't throw error, will happen later - -from . import LigandAtomMapping - - -# Helper Function / reducing code amount -def _get_all_mapped_atoms_with( - oeyMolA, - oeyMolB, - numMaxPossibleMappingAtoms: int, - criterium: Callable, -) -> int: - molA_allAtomsWith = len(list(filter(criterium, oeyMolA.GetAtoms()))) - molB_allAtomsWith = len(list(filter(criterium, oeyMolB.GetAtoms()))) - - if molA_allAtomsWith > molB_allAtomsWith and molA_allAtomsWith <= numMaxPossibleMappingAtoms: - numMaxPossibleMappings = molA_allAtomsWith - else: - numMaxPossibleMappings = molB_allAtomsWith - - return numMaxPossibleMappings - - -@requires_package("perses") -def default_perses_scorer( - mapping: LigandAtomMapping, - use_positions: bool = False, - normalize: bool = True, -) -> float: - """ - Score an atom mapping with the default Perses score function. - - Parameters - ---------- - mapping: LigandAtomMapping - is an OpenFE Ligand Mapping, that should be mapped - use_positions: bool, optional - if the positions are used, perses takes the inverse eucledian distance - of mapped atoms into account. - else the number of mapped atoms is used for the score. - default True - normalize: bool, optional - if true, the scores get normalized, such that different molecule pairs - can be compared for one scorer metric, default = True - *Warning* does not work for use_positions right now! - - Raises - ------ - NotImplementedError - Normalization of the score using positions is not implemented right - now. - - Returns - ------- - float - """ - warnings.warn( - "default_perses_scorer is deprecated and is planned to be removed in openfe v2.0. If you have questions related to this, please open an issue at https://github.com/OpenFreeEnergy/openfe/issues", - DeprecationWarning, - ) - - score = AtomMapper(use_positions=use_positions).score_mapping( - AtomMapping( - old_mol=mapping.componentA.to_openff(), - new_mol=mapping.componentB.to_openff(), - old_to_new_atom_map=mapping.componentA_to_componentB, - ) - ) - - # normalize - if normalize: - oeyMolA = mapping.componentA.to_openff().to_openeye() - oeyMolB = mapping.componentB.to_openff().to_openeye() - if use_positions: - raise NotImplementedError("normalizing using positions is not currently implemented") - else: - smallerMolecule = oeyMolA if (oeyMolA.NumAtoms() < oeyMolB.NumAtoms()) else oeyMolB - numMaxPossibleMappingAtoms = smallerMolecule.NumAtoms() - # Max possible Aromatic mappings - numMaxPossibleAromaticMappings = _get_all_mapped_atoms_with( - oeyMolA=oeyMolA, - oeyMolB=oeyMolB, - numMaxPossibleMappingAtoms=numMaxPossibleMappingAtoms, - criterium=lambda x: x.IsAromatic(), - ) - - # Max possible heavy mappings - numMaxPossibleHeavyAtomMappings = _get_all_mapped_atoms_with( - oeyMolA=oeyMolA, - oeyMolB=oeyMolB, - numMaxPossibleMappingAtoms=numMaxPossibleMappingAtoms, - criterium=lambda x: x.GetAtomicNum() > 1, - ) - - # Max possible ring mappings - numMaxPossibleRingMappings = _get_all_mapped_atoms_with( - oeyMolA=oeyMolA, - oeyMolB=oeyMolB, - numMaxPossibleMappingAtoms=numMaxPossibleMappingAtoms, - criterium=lambda x: x.IsInRing(), - ) - - # These weights are totally arbitrary - normalize_score = ( - 1.0 * numMaxPossibleMappingAtoms - + 0.8 * numMaxPossibleAromaticMappings - + 0.5 * numMaxPossibleHeavyAtomMappings - + 0.4 * numMaxPossibleRingMappings - ) - - score /= normalize_score # final normalize score - - return score diff --git a/src/openfe/tests/setup/atom_mapping/test_perses_atommapper.py b/src/openfe/tests/setup/atom_mapping/test_perses_atommapper.py deleted file mode 100644 index 15e00d661..000000000 --- a/src/openfe/tests/setup/atom_mapping/test_perses_atommapper.py +++ /dev/null @@ -1,71 +0,0 @@ -# This code is part of OpenFE and is licensed under the MIT license. -# For details, see https://github.com/OpenFreeEnergy/openfe -import pytest -from openff.units import unit -from openff.utilities.testing import skip_if_missing - -from openfe.setup.atom_mapping import LigandAtomMapping, PersesAtomMapper - - -@skip_if_missing("openeye") -@skip_if_missing("perses") -def test_simple(atom_mapping_basic_test_files): - # basic sanity check on the LigandAtomMapper - mol1 = atom_mapping_basic_test_files["methylcyclohexane"] - mol2 = atom_mapping_basic_test_files["toluene"] - with pytest.warns(DeprecationWarning, match="PersesAtomMapper"): - mapper = PersesAtomMapper() - - mapping_gen = mapper.suggest_mappings(mol1, mol2) - - mapping = next(mapping_gen) - assert isinstance(mapping, LigandAtomMapping) - # maps (CH3) off methyl and (6C + 5H) on ring - assert len(mapping.componentA_to_componentB) == 4 - - -@skip_if_missing("openeye") -@skip_if_missing("perses") -def test_generator_length(atom_mapping_basic_test_files): - # check that we get one mapping back from Lomap LigandAtomMapper then the - # generator stops correctly - mol1 = atom_mapping_basic_test_files["methylcyclohexane"] - mol2 = atom_mapping_basic_test_files["toluene"] - with pytest.warns(DeprecationWarning, match="PersesAtomMapper"): - mapper = PersesAtomMapper() - - mapping_gen = mapper.suggest_mappings(mol1, mol2) - - _ = next(mapping_gen) - with pytest.raises(StopIteration): - next(mapping_gen) - - -@skip_if_missing("openeye") -@skip_if_missing("perses") -def test_empty_atommappings(mol_pair_to_shock_perses_mapper): - mol1, mol2 = mol_pair_to_shock_perses_mapper - with pytest.warns(DeprecationWarning, match="PersesAtomMapper"): - mapper = PersesAtomMapper() - - mapping_gen = mapper.suggest_mappings(mol1, mol2) - - # The expected return is an empty mapping - assert len(list(mapping_gen)) == 0 - - with pytest.raises(StopIteration): - next(mapping_gen) - - -@skip_if_missing("openeye") -@skip_if_missing("perses") -def test_dict_round_trip(): - with pytest.warns(DeprecationWarning, match="PersesAtomMapper"): - # use some none defaults - mapper1 = PersesAtomMapper( - allow_ring_breaking=False, - preserve_chirality=False, - coordinate_tolerance=0.01 * unit.nanometer, - ) - mapper2 = PersesAtomMapper.from_dict(mapper1.to_dict()) - assert mapper2.to_dict() == mapper1.to_dict() diff --git a/src/openfe/tests/setup/atom_mapping/test_perses_scorers.py b/src/openfe/tests/setup/atom_mapping/test_perses_scorers.py deleted file mode 100644 index 0becbd0f6..000000000 --- a/src/openfe/tests/setup/atom_mapping/test_perses_scorers.py +++ /dev/null @@ -1,97 +0,0 @@ -# This code is part of OpenFE and is licensed under the MIT license. -# For details, see https://github.com/OpenFreeEnergy/openfe - -import numpy as np -import pytest -from numpy.testing import assert_, assert_allclose -from openff.utilities import skip_if_missing - -from openfe.setup import perses_scorers - -from ....utils.silence_root_logging import silence_root_logging - -USING_OLD_OFF = False - - -@skip_if_missing("openeye") -@skip_if_missing("perses") -@pytest.mark.xfail(not USING_OLD_OFF, reason="perses #1108") -def test_perses_normalization_not_using_positions(gufe_atom_mapping_matrix): - # now run the openfe equivalent with the same ligand atom _mappings - with pytest.warns(DeprecationWarning, match="default_perses_scorer"): - scorer = perses_scorers.default_perses_scorer - molecule_row = np.max(list(gufe_atom_mapping_matrix.keys())) + 1 - norm_scores = np.zeros([molecule_row, molecule_row]) - - for (i, j), ligand_atom_mapping in gufe_atom_mapping_matrix.items(): - norm_score = scorer( - ligand_atom_mapping, - use_positions=False, - normalize=True, - ) - norm_scores[i, j] = norm_scores[j, i] = norm_score - assert norm_scores.shape == (8, 8) - - assert_( - np.all((norm_scores <= 1) & (norm_scores >= 0.0)), - msg="OpenFE norm value larger than 1 or smaller than 0", - ) - - -@skip_if_missing("openeye") -@skip_if_missing("perses") -@pytest.mark.xfail(not USING_OLD_OFF, reason="perses #1108") -def test_perses_not_implemented_position_using(gufe_atom_mapping_matrix): - with pytest.warns(DeprecationWarning, match="default_perses_scorer"): - scorer = perses_scorers.default_perses_scorer - - first_key = list(gufe_atom_mapping_matrix.keys())[0] - match_re = "normalizing using positions is not currently implemented" - with pytest.raises(NotImplementedError, match=match_re): - norm_score = scorer( - gufe_atom_mapping_matrix[first_key], - use_positions=True, - normalize=True, - ) - - -@skip_if_missing("openeye") -@skip_if_missing("perses") -@pytest.mark.xfail(not USING_OLD_OFF, reason="perses #1108") -def test_perses_regression(gufe_atom_mapping_matrix): - with silence_root_logging(): - from perses.rjmc.atom_mapping import AtomMapper, AtomMapping - # This is the way how perses does scoring - molecule_row = np.max(list(gufe_atom_mapping_matrix.keys())) + 1 - matrix = np.zeros([molecule_row, molecule_row]) - for x in gufe_atom_mapping_matrix.items(): - (i, j), ligand_atom_mapping = x - # Build Perses Mapping: - perses_atom_mapping = AtomMapping( - old_mol=ligand_atom_mapping.componentA.to_openff(), - new_mol=ligand_atom_mapping.componentB.to_openff(), - old_to_new_atom_map=ligand_atom_mapping.componentA_to_componentB, - ) - # score Perses Mapping - Perses Style - matrix[i, j] = matrix[j, i] = AtomMapper().score_mapping(perses_atom_mapping) - - assert matrix.shape == (8, 8) - - # now run the openfe equivalent with the same ligand atom _mappings - with pytest.warns(DeprecationWarning, match="default_perses_scorer"): - scorer = perses_scorers.default_perses_scorer - scores = np.zeros_like(matrix) - for (i, j), ligand_atom_mapping in gufe_atom_mapping_matrix.items(): - score = scorer( - ligand_atom_mapping, - use_positions=True, - normalize=False, - ) - - scores[i, j] = scores[j, i] = score - - assert_allclose( - actual=matrix, - desired=scores, - err_msg="openFE was not close to perses", - )