From 2a00e740509ce523e800e55c25e80825b31856fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Sat, 25 Sep 2021 01:13:38 +0200 Subject: [PATCH 01/29] Add mapper and tests --- peleffy/tests/test_mapper.py | 85 ++++++++++++++ peleffy/topology/__init__.py | 1 + peleffy/topology/alchemy.py | 0 peleffy/topology/mapper.py | 124 ++++++++++++++++++++ peleffy/utils/toolkits.py | 211 +++++++++++++++++++++++++++++++++-- 5 files changed, 412 insertions(+), 9 deletions(-) create mode 100644 peleffy/tests/test_mapper.py create mode 100644 peleffy/topology/alchemy.py create mode 100644 peleffy/topology/mapper.py diff --git a/peleffy/tests/test_mapper.py b/peleffy/tests/test_mapper.py new file mode 100644 index 00000000..03685cca --- /dev/null +++ b/peleffy/tests/test_mapper.py @@ -0,0 +1,85 @@ +""" +This module contains the tests to check peleffy's molecular mapper. +""" + +import pytest + + +class TestMapper(object): + """ + It wraps all tests that involve Mapper class. + """ + + def test_mapper_initializer(self): + """ + It checks the initialization of the Mapper class. + """ + from peleffy.topology import Molecule + from peleffy.topology import Mapper + + mol1 = Molecule(smiles='c1ccccc1', hydrogens_are_explicit=False) + mol2 = Molecule(smiles='c1ccccc1C', hydrogens_are_explicit=False) + + # Check initializer with only the two molecules + mapper = Mapper(mol1, mol2) + + # Check initializer with only include_hydrogens parameter + mapper = Mapper(mol1, mol2, include_hydrogens=False) + + # Check initializer with bad types + with pytest.raises(TypeError): + mapper = Mapper(mol1.rdkit_molecule, mol2) + + with pytest.raises(TypeError): + mapper = Mapper(mol1, "mol2") + + def test_mapper_mapping(self): + """ + It validates the mapping. + """ + from peleffy.topology import Molecule + from peleffy.topology import Mapper + + # First mapping checker + mol1 = Molecule(smiles='c1ccccc1', hydrogens_are_explicit=False) + mol2 = Molecule(smiles='c1ccccc1C', hydrogens_are_explicit=False) + + mapper = Mapper(mol1, mol2, include_hydrogens=False) + mapping = mapper.get_mapping() + + assert mapping == [(0, 0), (1, 1), (2, 2), (3, 3), + (4, 4), (5, 5)], 'Unexpected mapping' + + # Second mapping checker + mol1 = Molecule(smiles='c1(C)ccccc1C', hydrogens_are_explicit=False) + mol2 = Molecule(smiles='c1c(C)cccc1C', hydrogens_are_explicit=False) + + mapper = Mapper(mol1, mol2, include_hydrogens=False) + mapping = mapper.get_mapping() + + assert mapping == [(0, 1), (1, 2), (2, 0), (3, 6), + (4, 5), (5, 4), (6, 3)], 'Unexpected mapping' + + # Third mapping checker with hydrogens + mol1 = Molecule(smiles='c1ccccc1', hydrogens_are_explicit=False) + mol2 = Molecule(smiles='c1ccccc1C', hydrogens_are_explicit=False) + + mapper = Mapper(mol1, mol2, include_hydrogens=True) + mapping = mapper.get_mapping() + + assert mapping == [(0, 0), (1, 1), (2, 2), (3, 3), + (4, 4), (5, 5), (11, 6), (10, 11), + (9, 10), (8, 9), (7, 8), (6, 7)], \ + 'Unexpected mapping' + + # Fourth mapping checker with hydrogens + mol1 = Molecule(smiles='c1(C)ccccc1C', hydrogens_are_explicit=False) + mol2 = Molecule(smiles='c1c(C)cccc1C', hydrogens_are_explicit=False) + + mapper = Mapper(mol1, mol2, include_hydrogens=True) + mapping = mapper.get_mapping() + + assert mapping == [(0, 1), (1, 2), (8, 9), (9, 10), + (10, 11), (2, 0), (3, 6), (4, 5), + (5, 4), (6, 3), (7, 12), (14, 13), + (13, 14), (12, 7), (11, 8)], 'Unexpected mapping' diff --git a/peleffy/topology/__init__.py b/peleffy/topology/__init__.py index b8dcd9cb..8d0abf06 100644 --- a/peleffy/topology/__init__.py +++ b/peleffy/topology/__init__.py @@ -4,3 +4,4 @@ from .zmatrix import ZMatrix from .rotamer import RotamerLibrary from .conformer import BCEConformations +from .mapper import Mapper diff --git a/peleffy/topology/alchemy.py b/peleffy/topology/alchemy.py new file mode 100644 index 00000000..e69de29b diff --git a/peleffy/topology/mapper.py b/peleffy/topology/mapper.py new file mode 100644 index 00000000..36c4a5a0 --- /dev/null +++ b/peleffy/topology/mapper.py @@ -0,0 +1,124 @@ +""" +This module handles all classes and functions related with the mapping +of two molecular topologies. +""" + + +class Mapper(object): + """ + It defines the Mapper class. + """ + + _TIMEOUT = 150 # Timeout to find the MCS, in seconds + + def __init__(self, molecule1, molecule2, include_hydrogens=True): + """ + Given two molecules, it finds the maximum common substructure + (MCS) and maps their atoms. + + Parameters + ---------- + molecule1: a peleffy.topology.Molecule + The first molecule to map + molecule2: a peleffy.topology.Molecule + The second molecule to map + include_hydrogens: bool + Whether to include hydrogen atoms in the mapping or not. + Default is True + """ + + # Check parameters + import peleffy + + if (not isinstance(molecule1, peleffy.topology.Molecule) + and not + isinstance(molecule1, peleffy.topology.molecule.Molecule)): + raise TypeError('Invalid input molecule 1') + + if (not isinstance(molecule2, peleffy.topology.Molecule) + and not + isinstance(molecule2, peleffy.topology.molecule.Molecule)): + raise TypeError('Invalid input molecule 2') + + if molecule1.rdkit_molecule is None: + raise ValueError('Molecule 1 has not been initialized') + + if molecule2.rdkit_molecule is None: + raise ValueError('Molecule 2 has not been initialized') + + self._molecule1 = molecule1 + self._molecule2 = molecule2 + self._include_hydrogens = include_hydrogens + + def get_mapping(self): + """ + It returns the mapping between both molecules. + + Returns + ------- + mapping : list[tuple] + The list of atom pairs between both molecules, represented + with tuples + """ + + from peleffy.utils.toolkits import RDKitToolkitWrapper + + rdkit_toolkit = RDKitToolkitWrapper() + + mcs_mol = rdkit_toolkit.get_mcs(self.molecule1, self.molecule2, + self._include_hydrogens, + self._TIMEOUT) + + mapping = rdkit_toolkit.get_atom_mapping(self.molecule1, + self.molecule2, + mcs_mol, + self._include_hydrogens) + + return mapping + + @property + def molecule1(self): + """ + It returns the first molecule to map. + + Returns + ------- + molecule1 : a peleffy.topology.Molecule + The first molecule to map + """ + return self._molecule1 + + @property + def molecule2(self): + """ + It returns the second molecule to map. + + Returns + ------- + molecule2 : a peleffy.topology.Molecule + The second molecule to map + """ + return self._molecule2 + + def _ipython_display_(self): + """ + It returns a representation of the mapping. + + Returns + ------- + mapping_representation : a IPython display object + Displayable RDKit molecules with mapping information + """ + from IPython.display import display + from peleffy.utils.toolkits import RDKitToolkitWrapper + + rdkit_toolkit = RDKitToolkitWrapper() + + mcs_mol = rdkit_toolkit.get_mcs(self.molecule1, self.molecule2, + self._include_hydrogens, + self._TIMEOUT) + + image = rdkit_toolkit.draw_mapping(self.molecule1, self.molecule2, + mcs_mol, self._include_hydrogens) + + return display(image) diff --git a/peleffy/utils/toolkits.py b/peleffy/utils/toolkits.py index 8cf07420..53b618a9 100644 --- a/peleffy/utils/toolkits.py +++ b/peleffy/utils/toolkits.py @@ -474,7 +474,7 @@ def to_pdb_file(self, molecule, path): for line in pdb_block.split('\n'): if line.startswith('HETATM'): renamed_pdb_block += line[:12] + names[atom_counter] \ - + ' ' + tag + line[20:] + '\n' + + ' ' + tag + line[20:] + '\n' atom_counter += 1 else: renamed_pdb_block += line + '\n' @@ -548,7 +548,7 @@ def get_atom_ids_with_rotatable_bonds(self, molecule): # Include missing rotatable bonds for amide groups for atom_pair in [frozenset(atom_pair) for atom_pair in rdkit_molecule.GetSubstructMatches( - Chem.MolFromSmarts('[$(N!@C(=O))]-&!@[!$(C(=O))&!D1&!$(*#*)]'))]: + Chem.MolFromSmarts('[$(N!@C(=O))]-&!@[!$(C(=O))&!D1&!$(*#*)]'))]: rot_bonds_atom_ids.add(atom_pair) # Remove bonds to terminal -CH3 @@ -634,7 +634,7 @@ def draw_molecule(self, representation, atom_indexes=list(), Parameters ---------- representation : an RDKit.molecule object - It is an RDKit molecule with an embeded 2D representation + It is an RDKit molecule with an embedded 2D representation Returns ------- @@ -659,6 +659,200 @@ def draw_molecule(self, representation, atom_indexes=list(), return image + def get_mcs(self, molecule1, molecule2, include_hydrogens, + timeout): + """ + Given two molecules, it finds the maximum common substructure + (MCS). + + Inspired by LOMAP repository, written by Gaetano Calabro and + David Mobley (https://github.com/MobleyLab/Lomap) + + Parameters + ---------- + molecule1 : a peleffy.topology.Molecule + The first molecule to map + molecule2 : a peleffy.topology.Molecule + The second molecule to map + include_hydrogens : bool + Whether to include hydrogen atoms in the mapping or not + timeout : int + The maximum time in seconds to compute the MCS + + Returns + ------- + mcs_mol : an RDKit.molecule object + The MCS molecule + """ + from rdkit import Chem + from rdkit.Chem import AllChem + from rdkit.Chem import rdFMCS + + rdkit_mol1 = deepcopy(molecule1.rdkit_molecule) + rdkit_mol2 = deepcopy(molecule2.rdkit_molecule) + + if not include_hydrogens: + rdkit_mol1 = AllChem.RemoveHs(rdkit_mol1) + rdkit_mol2 = AllChem.RemoveHs(rdkit_mol2) + + mcs = rdFMCS.FindMCS([rdkit_mol1, rdkit_mol2], + timeout=timeout, + atomCompare=rdFMCS.AtomCompare.CompareAny, + bondCompare=rdFMCS.BondCompare.CompareAny, + matchValences=False, + ringMatchesRingOnly=True, + completeRingsOnly=False, + matchChiralTag=False) + + # Checking + if mcs.canceled: + raise ValueError('Timeout! No MCS found between passed molecules') + + if mcs.numAtoms == 0: + raise ValueError('No MCS was found between the molecules') + + # The found MCS pattern (smart strings) is converted to a RDKit molecule + mcs_mol = Chem.MolFromSmarts(mcs.smartsString) + + try: + Chem.SanitizeMol(mcs_mol) + # if not, try to recover the atom aromaticity which is + # important for the ring counter + except Exception: + sanitize_failed = Chem.SanitizeMol( + mcs_mol, + sanitizeOps=Chem.SanitizeFlags.SANITIZE_SETAROMATICITY, + catchErrors=True) + if sanitize_failed: # if not, the MCS is skipped + raise ValueError('Sanitization Failed.') + + return mcs_mol + + def get_atom_mapping(self, molecule1, molecule2, mcs_mol, + include_hydrogens): + """ + Given two molecules and a third molecule representing their + maximum common substructure, it returns the atom mapping. + + Inspired by LOMAP repository, written by Gaetano Calabro and + David Mobley (https://github.com/MobleyLab/Lomap) + + Parameters + ---------- + molecule1 : a peleffy.topology.Molecule + The first molecule to map + molecule2 : a peleffy.topology.Molecule + The second molecule to map + mcs_mol : an RDKit.molecule object + The molecule representing the maximum common substructure + between molecule1 and molecule2 + include_hydrogens : bool + Whether to include hydrogen atoms in the mapping or not + + Returns + ------- + mapping : list[tuple] + The list of atom pairs between both molecules, represented + with tuples + """ + from rdkit.Chem import AllChem + + rdkit_mol1 = deepcopy(molecule1.rdkit_molecule) + rdkit_mol2 = deepcopy(molecule2.rdkit_molecule) + + if not include_hydrogens: + rdkit_mol1 = AllChem.RemoveHs(rdkit_mol1) + rdkit_mol2 = AllChem.RemoveHs(rdkit_mol2) + mcs_mol = AllChem.RemoveHs(mcs_mol) + + # Map atoms between mol1 and MCS mol + if rdkit_mol1.HasSubstructMatch(mcs_mol): + mol1_sub = rdkit_mol1.GetSubstructMatch(mcs_mol) + else: + raise ValueError('RDKit MCS Subgraph molecule 1 search failed') + + # Map atoms between mol2 and MCS mol + if rdkit_mol2.HasSubstructMatch(mcs_mol): + mol2_sub = rdkit_mol2.GetSubstructMatch(mcs_mol) + else: + raise ValueError('RDKit MCS Subgraph molecule 2 search failed') + + """ + if mcs_mol.HasSubstructMatch(mcs_mol): + _ = mcs_mol.GetSubstructMatch(mcs_mol) + else: + raise ValueError('RDKit MCS Subgraph search failed') + """ + + # Map between the two molecules + mapping = list(zip(mol1_sub, mol2_sub)) + + return mapping + + def draw_mapping(self, molecule1, molecule2, mcs_mol, + include_hydrogens): + """ + Given an atom mapping, it returns its representation. + + Parameters + ---------- + molecule1 : a peleffy.topology.Molecule + The first molecule to map + molecule2 : a peleffy.topology.Molecule + The second molecule to map + mcs_mol : an RDKit.molecule object + The MCS molecule + include_hydrogens : bool + Whether to include hydrogen atoms in the mapping or not + + Returns + ------- + image : an IPython's display object + The image of the atom mapping to display + """ + from rdkit import Chem + from rdkit.Chem import AllChem + from rdkit.Chem.Draw import rdMolDraw2D + from rdkit.Chem import Draw + from rdkit.Chem import rdFMCS + + rdkit_mol1 = deepcopy(molecule1.rdkit_molecule) + rdkit_mol2 = deepcopy(molecule2.rdkit_molecule) + + if not include_hydrogens: + rdkit_mol1 = AllChem.RemoveHs(rdkit_mol1) + rdkit_mol2 = AllChem.RemoveHs(rdkit_mol2) + + AllChem.Compute2DCoords(rdkit_mol1) + AllChem.Compute2DCoords(rdkit_mol2) + + mol1_name = '1: ' + molecule1.tag + mol2_name = '2: ' + molecule2.tag + + # Map atoms between mol1 and MCS mol + if rdkit_mol1.HasSubstructMatch(mcs_mol): + mol1_sub = rdkit_mol1.GetSubstructMatch(mcs_mol) + else: + raise ValueError('RDKit MCS Subgraph molecule 1 search failed') + + # Map atoms between mol2 and MCS mol + if rdkit_mol2.HasSubstructMatch(mcs_mol): + mol2_sub = rdkit_mol2.GetSubstructMatch(mcs_mol) + else: + raise ValueError('RDKit MCS Subgraph molecule 2 search failed') + + for atom in rdkit_mol1.GetAtoms(): + atom.SetProp('atomLabel', str(atom.GetIdx())) + for atom in rdkit_mol2.GetAtoms(): + atom.SetProp('atomLabel', str(atom.GetIdx())) + + image = Draw.MolsToGridImage([rdkit_mol1, rdkit_mol2], + molsPerRow=2, subImgSize=(300, 300), + legends=[mol1_name, mol2_name], + highlightAtomLists=[mol1_sub, mol2_sub]) + + return image + class AmberToolkitWrapper(ToolkitWrapper): """ @@ -693,7 +887,7 @@ def is_available(): ANTECHAMBER_PATH = find_executable("antechamber") if ANTECHAMBER_PATH is None: return False - if not(RDKitToolkitWrapper.is_available()): + if not (RDKitToolkitWrapper.is_available()): return False return True @@ -738,7 +932,7 @@ def compute_partial_charges(self, molecule, method='am1bcc'): with tempfile.TemporaryDirectory() as tmpdir: with temporary_cd(tmpdir): net_charge = off_molecule.total_charge / \ - unit.elementary_charge + unit.elementary_charge self._rdkit_toolkit_wrapper.to_sdf_file( molecule, tmpdir + '/molecule.sdf') @@ -901,7 +1095,7 @@ def get_parameters_from_forcefield(self, forcefield, molecule): molecule_parameters_list = forcefield.label_molecules(topology) assert len(molecule_parameters_list) == 1, 'A single molecule is ' \ - 'expected' + 'expected' return molecule_parameters_list[0] @@ -976,7 +1170,7 @@ def is_available(): is_installed : bool True if OpenForceField is installed, False otherwise. """ - if not(RDKitToolkitWrapper.is_available()): + if not (RDKitToolkitWrapper.is_available()): return False if SchrodingerToolkitWrapper.path_to_ffld_server() is None: @@ -1010,7 +1204,7 @@ def run_ffld_server(self, molecule): Parameters ---------- - molecule : an peleffy.topology.Molecule + molecule : a peleffy.topology.Molecule The peleffy's Molecule object Returns @@ -1023,7 +1217,6 @@ def run_ffld_server(self, molecule): with tempfile.TemporaryDirectory() as tmpdir: with temporary_cd(tmpdir): - self._rdkit_toolkit_wrapper.to_pdb_file( molecule, tmpdir + '/molecule.pdb') From 6e79527b8143aa2287f3dc837ff05c857461bd66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Sun, 26 Sep 2021 08:57:53 +0200 Subject: [PATCH 02/29] Add Alchemizer and tests --- peleffy/template/impact.py | 2 +- peleffy/tests/test_alchemy.py | 1851 +++++++++++++++++++++++++++++++++ peleffy/topology/__init__.py | 1 + peleffy/topology/alchemy.py | 864 +++++++++++++++ peleffy/topology/elements.py | 103 +- peleffy/topology/mapper.py | 25 +- peleffy/topology/rotamer.py | 4 +- 7 files changed, 2839 insertions(+), 11 deletions(-) create mode 100644 peleffy/tests/test_alchemy.py diff --git a/peleffy/template/impact.py b/peleffy/template/impact.py index ea929979..b67a8e6e 100644 --- a/peleffy/template/impact.py +++ b/peleffy/template/impact.py @@ -23,7 +23,7 @@ def __init__(self, topology): Parameters ---------- - topology : a Topology object + topology : a peleffy.topology.Topology object The molecular topology representation to write as a Impact template diff --git a/peleffy/tests/test_alchemy.py b/peleffy/tests/test_alchemy.py new file mode 100644 index 00000000..9186373c --- /dev/null +++ b/peleffy/tests/test_alchemy.py @@ -0,0 +1,1851 @@ +""" +This module contains tests that check that the alchemy module. +""" + +import pytest + + +def generate_molecules_and_topologies_from_pdb(pdb1, pdb2): + """ + It generates the molecules and topologies from two PDB files. + + Parameters + ---------- + pdb1 : str + The path to the first PDB + pdb2 : str + The path to the second PDB + + Returns + ------- + molecule1: a peleffy.topology.Molecule + The first molecule to map + molecule2: a peleffy.topology.Molecule + The second molecule to map + topology1 : a peleffy.topology.Topology object + The molecular topology representation of molecule 1 + topology2 : a peleffy.topology.Topology object + The molecular topology representation of molecule 2 + """ + from peleffy.topology import Molecule, Topology + from peleffy.forcefield import OpenForceField + from peleffy.utils import get_data_file_path + + mol1 = Molecule(get_data_file_path(pdb1)) + mol2 = Molecule(get_data_file_path(pdb2)) + + openff = OpenForceField('openff_unconstrained-2.0.0.offxml') + + params1 = openff.parameterize(mol1, charge_method='gasteiger') + params2 = openff.parameterize(mol2, charge_method='gasteiger') + + top1 = Topology(mol1, params1) + top2 = Topology(mol2, params2) + + return mol1, mol2, top1, top2 + + +def generate_molecules_and_topologies_from_smiles(smiles1, smiles2): + """ + It generates the molecules and topologies from two PDB files. + + Parameters + ---------- + smiles1 : str + The SMILES tag of the first molecule + smiles2 : str + The SMILES tag of the second molecule + + Returns + ------- + molecule1: a peleffy.topology.Molecule + The first molecule to map + molecule2: a peleffy.topology.Molecule + The second molecule to map + topology1 : a peleffy.topology.Topology object + The molecular topology representation of molecule 1 + topology2 : a peleffy.topology.Topology object + The molecular topology representation of molecule 2 + """ + from peleffy.topology import Molecule, Topology + from peleffy.forcefield import OpenForceField + + mol1 = Molecule(smiles=smiles1, hydrogens_are_explicit=False) + mol2 = Molecule(smiles=smiles2, hydrogens_are_explicit=False) + + openff = OpenForceField('openff_unconstrained-2.0.0.offxml') + + params1 = openff.parameterize(mol1, charge_method='gasteiger') + params2 = openff.parameterize(mol2, charge_method='gasteiger') + + top1 = Topology(mol1, params1) + top2 = Topology(mol2, params2) + + return mol1, mol2, top1, top2 + + +class TestAlchemy(object): + """Alchemy test.""" + + def test_alchemizer_initialization_checker(self): + """ + It checks the initialization checker of Alchemizer class. + """ + from peleffy.topology import Alchemizer + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles('C=C', + 'C(Cl)(Cl)(Cl)') + + _ = Alchemizer(top1, top2) + + with pytest.raises(TypeError): + _ = Alchemizer(mol1, top2) + + with pytest.raises(TypeError): + _ = Alchemizer(top1, mol2) + + @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, mapping, " + + "non_native_atoms, non_native_bonds, " + "non_native_angles, non_native_propers, " + "non_native_impropers, exclusive_atoms, " + "exclusive_bonds, exclusive_angles, " + "exclusive_propers, exclusive_impropers", + [(None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + [(0, 0), (1, 1), (2, 2), (3, 4)], + [6, ], + [], + [6, 7, 8], + [], + [], + [4, 5], + [3, 4], + [0, 1, 5], + [], + [0, ] + ), + (None, + None, + 'c1ccccc1', + 'c1ccccc1C', + [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), + (5, 5), (11, 6), (10, 11), (9, 10), (8, 9), + (7, 8), (6, 7)], + [12, 13, 14], + [], + [18, 19, 20, 21, 22, 23], + [], + [], + [], + [], + [], + [], + [] + ), + (None, + None, + 'c1ccccc1C', + 'c1ccccc1', + [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), + (5, 5), (6, 11), (11, 10), (10, 9), (9, 8), + (8, 7), (7, 6)], + [], + [], + [], + [], + [], + [12, 13, 14], + [12, 13, 14], + [18, 19, 20, 21, 22, 23], + [], + [] + ), + ('ligands/acetylene.pdb', + 'ligands/ethylene.pdb', + None, + None, + [(0, 0), (1, 1), (3, 4), (2, 2)], + [4, 5], + [], + [2, 3, 4, 5], + [1, 2], + [], + [], + [], + [], + [], + [] + ), + ('ligands/malonate.pdb', + 'ligands/propionic_acid.pdb', + None, + None, + [(0, 5), (1, 0), (2, 6), (3, 1), (4, 2), + (5, 4), (9, 10), (6, 3), (7, 8), (8, 9)], + [10], + [], + [13, 14, 15], + [23, 24], + [], + [], + [], + [], + [], + [] + ), + ('ligands/trimethylglycine.pdb', + 'ligands/propionic_acid.pdb', + None, + None, + [(0, 0), (1, 1), (4, 2), (17, 3), (5, 4), + (6, 10), (2, 8), (3, 9), (8, 5), (9, 6), + (10, 7)], + [], + [], + [], + [], + [], + [7, 11, 12, 13, 14, 15, 16, 18], + [7, 8, 9, 10, 11, 12, 15, 17], + [6, 7, 8, 9, 10, 11, 14, 19, 21, 22, 26, 27, + 28, 29, 30, 31, 32], + [40, 41], + [] + ), + ('ligands/trimethylglycine.pdb', + 'ligands/benzamidine.pdb', + None, + None, + [(1, 6), (0, 7), (8, 14), (9, 15), (2, 8), + (11, 16)], + [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], + [], + [33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 50, 51, 52], + [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, + 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78], + [1, 2, 3, 4, 5, 6, 7], + [3, 4, 5, 6, 7, 10, 12, 13, 14, 15, 16, 17, 18], + [3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + [1, 2, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, + 28, 29, 30, 31, 32], + [3, 4, 5, 6, 7, 8, 9, 10, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45], + [0] + ) + ]) + def test_alchemizer_initialization(self, pdb1, pdb2, smiles1, smiles2, + mapping, non_native_atoms, + non_native_bonds, non_native_angles, + non_native_propers, + non_native_impropers, + exclusive_atoms, exclusive_bonds, + exclusive_angles, exclusive_propers, + exclusive_impropers): + """ + It checks the initialization of Alchemizer class. + """ + from peleffy.topology import Alchemizer + + if pdb1 is not None and pdb2 is not None: + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_pdb(pdb1, pdb2) + elif smiles1 is not None and smiles2 is not None: + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles(smiles1, smiles2) + else: + raise ValueError('Invalid input parameters for the test') + + alchemizer = Alchemizer(top1, top2) + + # Check alchemizer content + assert alchemizer._mapping == mapping, \ + 'Unexpected mapping' + assert alchemizer._non_native_atoms == non_native_atoms, \ + 'Unexpected non native atoms' + assert alchemizer._non_native_bonds == non_native_bonds, \ + 'Unexpected non native bonds' + assert alchemizer._non_native_angles == non_native_angles, \ + 'Unexpected non native angles' + assert alchemizer._non_native_propers == non_native_propers, \ + 'Unexpected non native propers' + assert alchemizer._non_native_impropers == non_native_impropers, \ + 'Unexpected non native impropers' + assert alchemizer._exclusive_atoms == exclusive_atoms, \ + 'Unexpected exclusive atoms' + assert alchemizer._exclusive_bonds == exclusive_bonds, \ + 'Unexpected exclusive bonds' + assert alchemizer._exclusive_angles == exclusive_angles, \ + 'Unexpected exclusive angles' + assert alchemizer._exclusive_propers == exclusive_propers, \ + 'Unexpected exclusive propers' + assert alchemizer._exclusive_impropers == exclusive_impropers, \ + 'Unexpected exclusive impropers' + + def test_fep_lambda(self): + """ + It validates the effects of fep lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import (WritableAtom, WritableBond, + WritableAngle, WritableProper, + WritableImproper) + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles('C=C', + 'C(Cl)(Cl)(Cl)') + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert (sigma2 / sigma1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert (epsilon2 / epsilon1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert (SASA_radius2 / SASA_radius1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert (charge2 / charge1) - (1- 0.2) < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert (bond_sc2 / bond_sc1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert (angle_sc2 / angle_sc1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert (proper_c2 / proper_c1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert (improper_c2 / improper_c1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=1.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.4) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert (sigma2 / sigma1) - 0.4 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert (epsilon2 / epsilon1) - 0.4 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert (SASA_radius2 / SASA_radius1) - 0.4 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert (charge2 / charge1) - 0.4 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert (bond_sc2 / bond_sc1) - 0.4 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert (angle_sc2 / angle_sc1) - 0.4 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert (proper_c2 / proper_c1) - 0.4 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert (improper_c2 / improper_c1) - 0.4 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=1.0) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + def test_coul1_lambda(self): + """ + It validates the effects of coul1 lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import (WritableAtom, WritableBond, + WritableAngle, WritableProper, + WritableImproper) + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles('C=C', + 'C(Cl)(Cl)(Cl)') + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=0, + coul1_lambda=0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul1_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert (charge2 / charge1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul1_lambda=0.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul1_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul1_lambda=0.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=1.0, + coul1_lambda=1.0) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + def test_coul2_lambda(self): + """ + It validates the effects of coul2 lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import (WritableAtom, WritableBond, + WritableAngle, WritableProper, + WritableImproper) + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles('C=C', + 'C(Cl)(Cl)(Cl)') + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=0, + coul2_lambda=0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul2_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul2_lambda=1.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul2_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert (charge2 / charge1) - 0.8 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul2_lambda=0.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=1.0, + coul2_lambda=1.0) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + def test_vdw_lambda(self): + """ + It validates the effects of vdw lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import (WritableAtom, WritableBond, + WritableAngle, WritableProper, + WritableImproper) + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles('C=C', + 'C(Cl)(Cl)(Cl)') + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=0, + vdw_lambda=0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert (sigma2 / sigma1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert (epsilon2 / epsilon1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert (SASA_radius2 / SASA_radius1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=1.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert (sigma2 / sigma1) - 0.2 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert (epsilon2 / epsilon1) - 0.2 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert (SASA_radius2 / SASA_radius1) - 0.2 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=0.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=1.0, + vdw_lambda=1.0) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, " + + "fep_lambda, coul1_lambda, coul2_lambda, " + + "vdw_lambda, bonded_lambda, " + + "golden_sigmas, golden_epsilons, " + + "golden_born_radii, golden_SASA_radii, " + + "golden_nonpolar_gammas, " + + "golden_nonpolar_alphas", + [(None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.0, + None, + None, + None, + None, + [3.480646886945065, 3.480646886945065, + 2.5725815350632795, 2.5725815350632795, + 2.5725815350632795, 2.5725815350632795, 0.0], + [0.0868793154488, 0.0868793154488, + 0.01561134320353, 0.01561134320353, + 0.01561134320353, 0.01561134320353, 0.0], + [0, 0, 0, 0, 0, 0, 0], + [1.7403234434725325, 1.7403234434725325, + 1.2862907675316397, 1.2862907675316397, + 1.2862907675316397, 1.2862907675316397, 0.0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.2, + None, + None, + None, + None, + [3.480646886945065, 3.480646886945065, + 2.5725815350632795, 2.5725815350632795, + 2.0580652280506238, 2.0580652280506238, + 0.6615055612921249], + [0.0868793154488, 0.0868793154488, + 0.01561134320353, 0.01561134320353, + 0.012489074562824, + 0.012489074562824, 0.05312002093054], + [0, 0, 0, 0, 0, 0, 0], + [1.7403234434725325, 1.7403234434725325, + 1.2862907675316397, 1.2862907675316397, + 1.0290326140253119, 1.0290326140253119, + 0.33075278064606245], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.8, + None, + None, + None, + None, + [3.480646886945065, 3.480646886945065, + 2.5725815350632795, 2.5725815350632795, + 0.5145163070126558, 0.5145163070126558, + 2.6460222451684996], + [0.0868793154488, 0.0868793154488, + 0.01561134320353, 0.01561134320353, + 0.003122268640705999, 0.003122268640705999, + 0.21248008372216], + [0, 0, 0, 0, 0, 0, 0], + [1.7403234434725325, 1.7403234434725325, + 1.2862907675316397, 1.2862907675316397, + 0.2572581535063279, 0.2572581535063279, + 1.3230111225842498], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 1.0, + None, + None, + None, + None, + [3.480646886945065, 3.480646886945065, + 2.5725815350632795, 2.5725815350632795, + 0.0, 0.0, + 3.3075278064606244], + [0.0868793154488, 0.0868793154488, + 0.01561134320353, 0.01561134320353, 0.0, 0.0, + 0.2656001046527], + [0, 0, 0, 0, 0, 0, 0], + [1.7403234434725325, 1.7403234434725325, + 1.2862907675316397, 1.2862907675316397, 0.0, + 0.0, 1.6537639032303122], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0] + ), + ]) + def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, + fep_lambda, coul1_lambda, + coul2_lambda, vdw_lambda, + bonded_lambda, + golden_sigmas, + golden_epsilons, + golden_born_radii, + golden_SASA_radii, + golden_nonpolar_gammas, + golden_nonpolar_alphas): + """ + It validates the effects of lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import WritableAtom + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles(smiles1, smiles2) + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=fep_lambda, + coul1_lambda=coul1_lambda, + coul2_lambda=coul2_lambda, + vdw_lambda=vdw_lambda, + bonded_lambda=bonded_lambda) + + sigmas = list() + epsilons = list() + born_radii = list() + SASA_radii = list() + nonpolar_gammas = list() + nonpolar_alphas = list() + + for atom in top.atoms: + atom = WritableAtom(atom) + sigmas.append(atom.sigma) + epsilons.append(atom.epsilon) + born_radii.append(atom.born_radius) + SASA_radii.append(atom.SASA_radius) + nonpolar_gammas.append(atom.nonpolar_gamma) + nonpolar_alphas.append(atom.nonpolar_alpha) + + assert sigmas == golden_sigmas, 'Unexpected sigmas' + assert epsilons == golden_epsilons, 'Unexpected epsilons' + assert born_radii == golden_born_radii, 'Unexpected born radii' + assert SASA_radii == golden_SASA_radii, 'Unexpected SASA radii' + assert nonpolar_gammas == golden_nonpolar_gammas, \ + 'Unexpected non polar gammas' + assert nonpolar_alphas == golden_nonpolar_alphas, \ + 'Unexpected non polar alphas' diff --git a/peleffy/topology/__init__.py b/peleffy/topology/__init__.py index 8d0abf06..abba9c7e 100644 --- a/peleffy/topology/__init__.py +++ b/peleffy/topology/__init__.py @@ -5,3 +5,4 @@ from .rotamer import RotamerLibrary from .conformer import BCEConformations from .mapper import Mapper +from .alchemy import Alchemizer diff --git a/peleffy/topology/alchemy.py b/peleffy/topology/alchemy.py index e69de29b..5e2868fc 100644 --- a/peleffy/topology/alchemy.py +++ b/peleffy/topology/alchemy.py @@ -0,0 +1,864 @@ +""" +This module contains classes and methods related with alchemical modifications for molecular topologies. +""" + + +from abc import ABC +from dataclasses import dataclass + + +class Alchemizer(object): + """ + It defines the Alchemizer class. + """ + + def __init__(self, topology1, topology2): + """ + It initializes an Alchemizer object, which generates alchemical + representations considering the topologies of two different + molecules. + + Parameters + ---------- + topology1 : a peleffy.topology.Topology object + The molecular topology representation of molecule 1 + topology2 : a peleffy.topology.Topology object + The molecular topology representation of molecule 2 + """ + + # Check topologies + import peleffy + + if (not isinstance(topology1, peleffy.topology.Topology) + and not + isinstance(topology1, peleffy.topology.topology.Topology)): + raise TypeError('Invalid input topology 1') + + if (not isinstance(topology2, peleffy.topology.Topology) + and not + isinstance(topology2, peleffy.topology.topology.Topology)): + raise TypeError('Invalid input topology 2') + + self._topology1 = topology1 + self._topology2 = topology2 + self._molecule1 = topology1.molecule + self._molecule2 = topology2.molecule + + from peleffy.topology import Mapper + + # Map atoms from both molecules + mapper = Mapper(self.molecule1, self.molecule2, + include_hydrogens=True) + + self._mapping = mapper.get_mapping() + self._mcs_mol = mapper.get_mcs() + + # Join the two topologies + self._joint_topology, self._non_native_atoms, \ + self._non_native_bonds, self._non_native_angles, \ + self._non_native_propers, self._non_native_impropers = \ + self._join_topologies() + + # Get exclusive topological elements in topology 1 + self._exclusive_atoms, self._exclusive_bonds, \ + self._exclusive_angles, self._exclusive_propers, \ + self._exclusive_impropers = self._get_exclusive_elements() + + @property + def topology1(self): + """ + It returns the first peleffy's Topology. + + Returns + ------- + topology1 : a peleffy.topology.Topology + The first peleffy's Topology object + """ + return self._topology1 + + @property + def topology2(self): + """ + It returns the second peleffy's Topology. + + Returns + ------- + topology2 : a peleffy.topology.Topology + The second peleffy's Topology object + """ + return self._topology2 + + @property + def molecule1(self): + """ + It returns the first molecule. + + Returns + ------- + molecule1 : a peleffy.topology.Molecule + The first Molecule object + """ + return self._molecule1 + + @property + def molecule2(self): + """ + It returns the second molecule. + + Returns + ------- + molecule2 : a peleffy.topology.Molecule + The second Molecule object + """ + return self._molecule2 + + @property + def mapping(self): + """ + It returns the mapping between both molecules. + + Returns + ------- + mapping : list[tuple] + The list of atom pairs between both molecules, represented + with tuples + """ + return self._mapping + + @property + def mcs_mol(self): + """ + It returns the Maximum Common Substructure (MCS) between + both molecules. + + Parameters + ---------- + mcs_mol : an RDKit.molecule object + The resulting MCS molecule + """ + return self._mcs_mol + + def get_alchemical_topology(self, fep_lambda=None, coul1_lambda=None, + coul2_lambda=None, vdw_lambda=None, + bonded_lambda=None): + """ + Given a lambda, it returns an alchemical topology after + combining both input topologies. + + Parameters + ---------- + fep_lambda : float + The value to define an FEP lambda. This lambda affects + all the parameters. It needs to be contained between + 0 and 1. Default is None + coul1_lambda : float + The value to define a coulombic lambda for exclusive atoms + of molecule 1. This lambda only affects coulombic parameters + of exclusive atoms of molecule 1. It needs to be contained + between 0 and 1. Default is None + coul2_lambda : float + The value to define a coulombic lambda for exclusive atoms + of molecule 2. This lambda only affects coulombic parameters + of exclusive atoms of molecule 2. It needs to be contained + between 0 and 1. Default is None + vdw_lambda : float + The value to define a vdw lambda. This lambda only + affects van der Waals parameters. It needs to be contained + between 0 and 1. Default is None + bonded_lambda : float + The value to define a coulombic lambda. This lambda only + affects bonded parameters. It needs to be contained + between 0 and 1. Default is None + + Returns + ------- + alchemical_topology : a peleffy.topology.Topology + The resulting alchemical topology + """ + # Define lambdas + fep_lambda = FEPLambda(fep_lambda) + coul1_lambda = Coulombic1Lambda(coul1_lambda) + coul2_lambda = Coulombic2Lambda(coul2_lambda) + vdw_lambda = VanDerWaalsLambda(vdw_lambda) + bonded_lambda = BondedLambda(bonded_lambda) + + lambda_set = LambdaSet(fep_lambda, coul1_lambda, coul2_lambda, + vdw_lambda, bonded_lambda) + + alchemical_topology = self.topology_from_lambda_set(lambda_set) + + return alchemical_topology + + def topology_from_lambda_set(self, lambda_set): + """ + Given a lambda, it returns an alchemical topology after + combining both input topologies. + + Parameters + ---------- + lambda_set : a peleffy.topology.alchemy.LambdaSet object + The set of lambdas to use in the generation of the + alchemical topology + + Returns + ------- + alchemical_topology : a peleffy.topology.Topology + The resulting alchemical topology + """ + from copy import deepcopy + + alchemical_topology = deepcopy(self._joint_topology) + + for atom_idx, atom in enumerate(alchemical_topology.atoms): + if atom_idx in self._exclusive_atoms: + atom.apply_lambda(["sigma", "epsilon", "born_radius", + "SASA_radius", "nonpolar_gamma", + "nonpolar_alpha"], + lambda_set.get_lambda_for_vdw(), + reverse=False) + atom.apply_lambda(["charge"], + lambda_set.get_lambda_for_coulomb1(), + reverse=False) + + if atom_idx in self._non_native_atoms: + atom.apply_lambda(["sigma", "epsilon", "born_radius", + "SASA_radius", "nonpolar_gamma", + "nonpolar_alpha"], + lambda_set.get_lambda_for_vdw(), + reverse=True) + atom.apply_lambda(["charge"], + lambda_set.get_lambda_for_coulomb2(), + reverse=True) + + for bond_idx, bond in enumerate(alchemical_topology.bonds): + if bond_idx in self._exclusive_bonds: + bond.apply_lambda(["spring_constant"], + lambda_set.get_lambda_for_bonded(), + reverse=False) + + if bond_idx in self._non_native_bonds: + bond.apply_lambda(["spring_constant"], + lambda_set.get_lambda_for_bonded(), + reverse=True) + + for angle_idx, angle in enumerate(alchemical_topology.angles): + if angle_idx in self._exclusive_angles: + angle.apply_lambda(["spring_constant"], + lambda_set.get_lambda_for_bonded(), + reverse=False) + + if angle_idx in self._non_native_angles: + angle.apply_lambda(["spring_constant"], + lambda_set.get_lambda_for_bonded(), + reverse=True) + + for proper_idx, proper in enumerate(alchemical_topology.propers): + if proper_idx in self._exclusive_propers: + proper.apply_lambda(["spring_constant"], + lambda_set.get_lambda_for_bonded(), + reverse=False) + + if proper_idx in self._non_native_propers: + proper.apply_lambda(["spring_constant"], + lambda_set.get_lambda_for_bonded(), + reverse=True) + + for improper_idx, improper in enumerate(alchemical_topology.impropers): + if improper_idx in self._exclusive_propers: + improper.apply_lambda(["spring_constant"], + lambda_set.get_lambda_for_bonded(), + reverse=False) + + if improper_idx in self._non_native_propers: + improper.apply_lambda(["spring_constant"], + lambda_set.get_lambda_for_bonded(), + reverse=True) + + return alchemical_topology + + def _join_topologies(self): + """ + It joins the both topologies into a single one that contains + all topological elements. + + Returns + ------- + joint_topology : a peleffy.topology.Topology + The resulting alchemical topology + non_native_atoms : list[int] + The list of atom indices that were added to topology 1 + non_native_bonds : list[int] + The list of bond indices that were added to topology 1 + non_native_angles : list[int] + The list of angle indices that were added to topology 1 + non_native_propers : list[int] + The list of proper indices that were added to topology 1 + non_native_impropers : list[int] + The list of improper indices that were added to topology 1 + """ + from copy import deepcopy + from peleffy.topology import Topology + from peleffy.utils.toolkits import RDKitToolkitWrapper + from peleffy.utils import Logger + + # General initializers + logger = Logger() + rdkit_wrapper = RDKitToolkitWrapper() + + # First initialize the joint topology with topology 1 + joint_topology = deepcopy(self.topology1) + + # Initialize list of non native topological elements + non_native_atoms = list() + non_native_bonds = list() + non_native_angles = list() + non_native_propers = list() + non_native_impropers = list() + + # Define mappers + mol1_mapped_atoms = [atom_pair[0] for atom_pair in self.mapping] + mol2_mapped_atoms = [atom_pair[1] for atom_pair in self.mapping] + mol2_to_mol1_map = dict(zip(mol2_mapped_atoms, mol1_mapped_atoms)) + + # Add atoms from topology 2 that are missing in topology 1 + atom_names = self.molecule1.get_pdb_atom_names() + mol2_elements = rdkit_wrapper.get_elements(self.molecule2) + mol2_to_alc_map = dict() + for atom_idx, atom in enumerate(self.topology2.atoms): + if atom_idx not in mol2_mapped_atoms: + new_atom = deepcopy(atom) + + # Handle index of new atom + new_index = len(joint_topology.atoms) + new_atom.set_index(new_index) + mol2_to_alc_map[atom_idx] = new_index + + # Handle name of new atom + counter = 1 + new_name = '{:^4}'.format(str(mol2_elements[atom_idx]) + + str(counter)) + while new_name in atom_names: + counter += 1 + new_name = '{:^4}'.format(str(mol2_elements[atom_idx]) + + str(counter)) + atom_names.append(new_name) + new_atom.set_PDB_name(new_name) + + # Handle core allocation of new atom + new_atom.set_as_branch() + + # Add new atom to the alchemical topology + non_native_atoms.append(new_index) + joint_topology.add_atom(new_atom) + + else: + mol2_to_alc_map[atom_idx] = mol2_to_mol1_map[atom_idx] + + # Get an arbitrary absolute parent for molecule 2 (as long as it is in the MCS) + absolute_parent = None + for atom_idx in range(0, len(self.topology2.atoms)): + if atom_idx in mol2_mapped_atoms: + absolute_parent = atom_idx + break + else: + logger.error(['Error: no atom in the MCS found in molecule ' + + f'{self.molecule1.name}']) + + # Get parent ids according to this new absolute parent of molecule 2 + mol2_parent_idxs = self.molecule2.graph.get_parents(absolute_parent) + + # Assign parents + for atom_idx, atom in enumerate(self.topology2.atoms): + if atom_idx not in mol2_mapped_atoms: + alc_child_idx = mol2_to_alc_map[atom_idx] + alc_parent_idx = mol2_to_alc_map[mol2_parent_idxs[atom_idx]] + alc_parent_atom = joint_topology.atoms[alc_parent_idx] + joint_topology.atoms[alc_child_idx].set_parent( + alc_parent_atom) + + # Add bonds + for bond in self.topology2.bonds: + atom1_idx = bond.atom1_idx + atom2_idx = bond.atom2_idx + + if (atom1_idx not in mol2_mapped_atoms or + atom2_idx not in mol2_mapped_atoms): + new_bond = deepcopy(bond) + + # Handle index of new atom + new_index = len(joint_topology.bonds) + new_bond.set_index(new_index) + + # Handle atom indices + new_bond.set_atom1_idx(mol2_to_alc_map[atom1_idx]) + new_bond.set_atom2_idx(mol2_to_alc_map[atom2_idx]) + + # Add new bond to the alchemical topology + joint_topology.add_bond(new_bond) + + # Add angles + for angle in self.topology2.angles: + atom1_idx = angle.atom1_idx + atom2_idx = angle.atom2_idx + atom3_idx = angle.atom3_idx + + if (atom1_idx not in mol2_mapped_atoms or + atom2_idx not in mol2_mapped_atoms or + atom3_idx not in mol2_mapped_atoms): + new_angle = deepcopy(angle) + + # Handle index of new atom + new_index = len(joint_topology.angles) + new_angle.set_index(new_index) + + # Handle atom indices + new_angle.set_atom1_idx(mol2_to_alc_map[atom1_idx]) + new_angle.set_atom2_idx(mol2_to_alc_map[atom2_idx]) + new_angle.set_atom3_idx(mol2_to_alc_map[atom3_idx]) + + # Add new bond to the alchemical topology + non_native_angles.append(new_index) + joint_topology.add_angle(new_angle) + + # Add propers + for proper in self.topology2.propers: + atom1_idx = proper.atom1_idx + atom2_idx = proper.atom2_idx + atom3_idx = proper.atom3_idx + atom4_idx = proper.atom1_idx + + if (atom1_idx not in mol2_mapped_atoms or + atom2_idx not in mol2_mapped_atoms or + atom3_idx not in mol2_mapped_atoms or + atom4_idx not in mol2_mapped_atoms): + new_proper = deepcopy(proper) + + # Handle index of new atom + new_index = len(joint_topology.propers) + new_proper.set_index(new_index) + + # Handle atom indices + new_proper.set_atom1_idx(mol2_to_alc_map[atom1_idx]) + new_proper.set_atom2_idx(mol2_to_alc_map[atom2_idx]) + new_proper.set_atom3_idx(mol2_to_alc_map[atom3_idx]) + new_proper.set_atom4_idx(mol2_to_alc_map[atom4_idx]) + + # Add new bond to the alchemical topology + non_native_propers.append(new_index) + joint_topology.add_proper(new_proper) + + # Add impropers + for improper in self.topology2.impropers: + atom1_idx = improper.atom1_idx + atom2_idx = improper.atom2_idx + atom3_idx = improper.atom3_idx + atom4_idx = improper.atom1_idx + + if (atom1_idx not in mol2_mapped_atoms or + atom2_idx not in mol2_mapped_atoms or + atom3_idx not in mol2_mapped_atoms or + atom4_idx not in mol2_mapped_atoms): + new_improper = deepcopy(improper) + + # Handle index of new atom + new_index = len(joint_topology.impropers) + new_improper.set_index(new_index) + + # Handle atom indices + new_improper.set_atom1_idx(mol2_to_alc_map[atom1_idx]) + new_improper.set_atom2_idx(mol2_to_alc_map[atom2_idx]) + new_improper.set_atom3_idx(mol2_to_alc_map[atom3_idx]) + new_improper.set_atom4_idx(mol2_to_alc_map[atom4_idx]) + + # Add new bond to the alchemical topology + non_native_impropers.append(new_index) + joint_topology.add_improper(new_improper) + + return joint_topology, non_native_atoms, non_native_bonds, \ + non_native_angles, non_native_propers, non_native_impropers + + def _get_exclusive_elements(self): + """ + It identifies those topological elements that are exclusive + of topology 1. The condition to be exclusive is to belong to + topology 1 but not to the MCS. + + Returns + ------- + exclusive_atoms : list[int] + The list of atom indices that are exclusive of topology 1 + exclusive_bonds : list[int] + The list of bond indices that are exclusive of topology 1 + exclusive_angles : list[int] + The list of angle indices that are exclusive of topology 1 + exclusive_propers : list[int] + The list of proper indices that are exclusive of topology 1 + exclusive_impropers : list[int] + The list of improper indices that are exclusive of topology 1 + """ + # Define mappers + mol1_mapped_atoms = [atom_pair[0] for atom_pair in self.mapping] + + # Initialize list of exclusive topological elements + exclusive_atoms = list() + exclusive_bonds = list() + exclusive_angles = list() + exclusive_propers = list() + exclusive_impropers = list() + + # Identify atoms + for atom_idx, atom in enumerate(self.topology1.atoms): + if atom_idx not in mol1_mapped_atoms: + exclusive_atoms.append(atom_idx) + + # Identify bonds + for bond_idx, bond in enumerate(self.topology1.bonds): + atom1_idx = bond.atom1_idx + atom2_idx = bond.atom2_idx + + if (atom1_idx not in mol1_mapped_atoms or + atom2_idx not in mol1_mapped_atoms): + exclusive_bonds.append(bond_idx) + + # Identify angles + for angle_idx, angle in enumerate(self.topology1.angles): + atom1_idx = angle.atom1_idx + atom2_idx = angle.atom2_idx + atom3_idx = angle.atom3_idx + + if (atom1_idx not in mol1_mapped_atoms or + atom2_idx not in mol1_mapped_atoms or + atom3_idx not in mol1_mapped_atoms): + exclusive_angles.append(angle_idx) + + # Identify propers + for proper_idx, proper in enumerate(self.topology1.propers): + atom1_idx = proper.atom1_idx + atom2_idx = proper.atom2_idx + atom3_idx = proper.atom3_idx + atom4_idx = proper.atom3_idx + + if (atom1_idx not in mol1_mapped_atoms or + atom2_idx not in mol1_mapped_atoms or + atom3_idx not in mol1_mapped_atoms or + atom4_idx not in mol1_mapped_atoms): + exclusive_propers.append(proper_idx) + + # Identify impropers + for improper_idx, improper in enumerate(self.topology1.impropers): + atom1_idx = improper.atom1_idx + atom2_idx = improper.atom2_idx + atom3_idx = improper.atom3_idx + atom4_idx = improper.atom3_idx + + if (atom1_idx not in mol1_mapped_atoms or + atom2_idx not in mol1_mapped_atoms or + atom3_idx not in mol1_mapped_atoms or + atom4_idx not in mol1_mapped_atoms): + exclusive_impropers.append(improper_idx) + + return exclusive_atoms, exclusive_bonds, exclusive_angles, \ + exclusive_propers, exclusive_impropers + + +class Lambda(ABC): + """ + It defines the Lambda class. + """ + + _TYPE = "" + + def __init__(self, value=None): + """ + It initializes a Lambda object. + + Parameters + ---------- + value : float + The value of this Lambda object. It needs to be + contained between 0 and 1. Default is None + """ + if value is not None: + try: + value = float(value) + except ValueError: + raise ValueError("Invalid value for a lambda: " + + f"\'{value}\'") + if (value > 1) or (value < 0): + raise ValueError("Invalid value for a lambda: " + + f"\'{value}\'. " + + "It has to be between 0 and 1") + + self._value = value + pass + + @property + def value(self): + """ + It returns the value of this Lambda object. + + Returns + ------- + value : float or None + The value of this Lambda object. It can be None if the + value for this Lambda object has not been set + """ + return self._value + + @property + def type(self): + """ + It returns the type of this Lambda object. + + Returns + ------- + type : str + The type of this Lambda object + """ + return self._TYPE + + @property + def is_set(self): + """ + It answers whether the value of this Lambda object has + been set or not. + + Returns + ------- + is_set : bool + It is true only if the value of Lambda object has been + set + """ + return self.value is not None + + +class FEPLambda(Lambda): + """ + It defines the FEPLambda class. It affects all parameters. + """ + _TYPE = "fep" + + +class Coulombic1Lambda(Lambda): + """ + It defines the CoulombicLambda1 class. It affects only coulombic + parameters involving exclusive atoms of molecule 1. + """ + _TYPE = "coulombic1" + + +class Coulombic2Lambda(Lambda): + """ + It defines the CoulombicLambda2 class. It affects only coulombic + parameters involving exclusive atoms of molecule 2. + """ + _TYPE = "coulombic2" + + +class VanDerWaalsLambda(Lambda): + """ + It defines the VanDerWaalsLambda class. It affects only van der Waals + parameters. + """ + _TYPE = "vdw" + + +class BondedLambda(Lambda): + """ + It defines the BondedLambda class. It affects only bonded parameters. + """ + _TYPE = "bonded" + + +class LambdaSet(object): + """ + It defines the LambdaSet class. + """ + + def __init__(self, fep_lambda, coul1_lambda, coul2_lambda, + vdw_lambda, bonded_lambda): + """ + It initializes a LambdaSet object which stores all the different + types of lambda. + + Parameters + ---------- + fep_lambda : a peleffy.topology.alchemy.FEPLambda object + The fep lambda + coul1_lambda : a peleffy.topology.alchemy.CoulombicLambda object + The coulombic lambda for exclusive atoms of molecule 1 + coul2_lambda : a peleffy.topology.alchemy.CoulombicLambda object + The coulombic lambda for exclusive atoms of molecule 2 + vdw_lambda : a peleffy.topology.alchemy.VanDerWaalsLambda object + The van der Waals lambda + bonded_lambda : a peleffy.topology.alchemy.BondedLambda object + The bonded lambda + """ + import peleffy + + # Check parameters + if not isinstance(fep_lambda, + peleffy.topology.alchemy.FEPLambda): + raise TypeError('Invalid fep_lambda supplied to LambdaSet') + if not isinstance(coul1_lambda, + peleffy.topology.alchemy.Coulombic1Lambda): + raise TypeError('Invalid coul1_lambda supplied to LambdaSet') + if not isinstance(coul2_lambda, + peleffy.topology.alchemy.Coulombic2Lambda): + raise TypeError('Invalid coul2_lambda supplied to LambdaSet') + if not isinstance(vdw_lambda, + peleffy.topology.alchemy.VanDerWaalsLambda): + raise TypeError('Invalid vdw_lambda supplied to LambdaSet') + if not isinstance(bonded_lambda, + peleffy.topology.alchemy.BondedLambda): + raise TypeError('Invalid bonded_lambda supplied to LambdaSet') + + self._fep_lambda = fep_lambda + self._coul1_lambda = coul1_lambda + self._coul2_lambda = coul2_lambda + self._vdw_lambda = vdw_lambda + self._bonded_lambda = bonded_lambda + + @property + def fep_lambda(self): + """ + It returns the fep_lambda value. + + Returns + ------- + fep_lambda : float + The value of the fep_lambda + """ + return self._fep_lambda + + @property + def coul1_lambda(self): + """ + It returns the coul1_lambda value. + + Returns + ------- + coul1_lambda : float + The value of the coul1_lambda + """ + return self._coul1_lambda + + @property + def coul2_lambda(self): + """ + It returns the coul2_lambda value. + + Returns + ------- + coul2_lambda : float + The value of the coul2_lambda + """ + return self._coul2_lambda + + @property + def vdw_lambda(self): + """ + It returns the vdw_lambda value. + + Returns + ------- + vdw_lambda : float + The value of the vdw_lambda + """ + return self._vdw_lambda + + @property + def bonded_lambda(self): + """ + It returns the bonded_lambda value. + + Returns + ------- + bonded_lambda : float + The value of the bonded_lambda + """ + return self._bonded_lambda + + def get_lambda_for_vdw(self): + """ + It returns the lambda to be applied on van der Waals parameters. + + Returns + ------- + lambda_value : float + The lambda value to be applied on van der Waals parameters + """ + if self.vdw_lambda.is_set: + lambda_value = self.vdw_lambda.value + + elif self.fep_lambda.is_set: + lambda_value = self.fep_lambda.value + + else: + lambda_value = 0.0 + + return lambda_value + + def get_lambda_for_coulomb1(self): + """ + It returns the lambda to be applied on Coulomb parameters of + exclusive atoms of molecule 1. + + Returns + ------- + lambda_value : float + The lambda value to be applied on Coulomb parameters of + exclusive atoms of molecule 1 + """ + if self.coul1_lambda.is_set: + lambda_value = self.coul1_lambda.value + + elif self.fep_lambda.is_set: + lambda_value = self.fep_lambda.value + + else: + lambda_value = 0.0 + + return lambda_value + + def get_lambda_for_coulomb2(self): + """ + It returns the lambda to be applied on Coulomb parameters of + exclusive atoms of molecule 2. + + Returns + ------- + lambda_value : float + The lambda value to be applied on Coulomb parameters of + exclusive atoms of molecule 2 + """ + if self.coul2_lambda.is_set: + lambda_value = self.coul2_lambda.value + + elif self.fep_lambda.is_set: + lambda_value = self.fep_lambda.value + + else: + lambda_value = 0.0 + + return lambda_value + + def get_lambda_for_bonded(self): + """ + It returns the lambda to be applied on bonded parameters. + + Returns + ------- + lambda_value : float + The lambda value to be applied on bonded parameters + """ + if self.bonded_lambda.is_set: + lambda_value = self.bonded_lambda.value + + elif self.fep_lambda.is_set: + lambda_value = self.fep_lambda.value + + else: + lambda_value = 0.0 + + return lambda_value diff --git a/peleffy/topology/elements.py b/peleffy/topology/elements.py index 3613160b..9793914f 100644 --- a/peleffy/topology/elements.py +++ b/peleffy/topology/elements.py @@ -13,6 +13,7 @@ class _TopologyElement(object): _name = None _writable_attrs = [] + _lambda_changeable = [] class TopologyIterator(object): """ @@ -78,6 +79,48 @@ def n_writable_attrs(self): """ return len(self._writable_attrs) + def apply_lambda(self, attributes_to_modify, lambda_value, + reverse=False): + """ + Given a lambda value, it modifies a set of attributes of + this topological element. A lambda equal to 0 will keep the + original attributes of this topological element. A lambda + equal to 1 will remove completely the effects of the + attributes. This behavior will be the opposite when reverse + is set to True. + + Parameters + ---------- + attributes_to_modify : list[str] + The list attribute names that will be modified + lambda_value : float + A value between 0 and 1 that defined the lambda to + apply to the attributes + reverse : bool + When set to true the effects of lambda will be the + opposite + """ + from peleffy.utils import Logger + + logger = Logger() + + if not reverse: + lambda_value = 1.0 - lambda_value + + for attribute in attributes_to_modify: + if attribute not in self._lambda_changeable: + logger.error([f'Attribute {attribute} is not an ' + f'attribute that can be changed with ' + f'a lambda. It will not be changed.']) + continue + + value = getattr(self, '_' + attribute) + + if value is not None: + value = value * lambda_value + + setattr(self, '_' + attribute, value) + def __iter__(self): """ It returns an instance of the TopologyIterator. @@ -126,6 +169,8 @@ class Atom(_TopologyElement): _name = 'Atom' _writable_attrs = ['index', 'PDB_name', 'OPLS_type'] + _lambda_changeable = ['sigma', 'epsilon', 'charge', 'born_radius', + 'SASA_radius', 'nonpolar_gamma', 'nonpolar_alpha'] def __init__(self, index=-1, core=None, OPLS_type=None, PDB_name=None, unknown=None, x=None, y=None, z=None, sigma=None, @@ -198,13 +243,24 @@ def set_index(self, index): self._index = index def set_as_core(self): - """It sets the atom as core""" + """It sets the atom as core.""" self._core = True def set_as_branch(self): - """It sets the atom as branch""" + """It sets the atom as branch.""" self._core = False + def set_PDB_name(self, pdb_name): + """ + It sets the PDB atom name. + + Parameters + ---------- + pdb_name : str + The PDB atom name to be set + """ + self._PDB_name = pdb_name + def set_parent(self, parent): """ It sets the parent of the atom. @@ -518,8 +574,9 @@ def parent(self): Returns ------- - parent : simtk.unit.Quantity - The nonpolar gamma parameter of this Atom object + parent : a peleffy.topology.Atom or None + The parent of the atom. If it is the absolute parent, it + will return a None """ return self._parent @@ -555,6 +612,7 @@ class Bond(_TopologyElement): _name = 'Bond' _writable_attrs = ['atom1_idx', 'atom2_idx', 'spring_constant', 'eq_dist'] + _lambda_changeable = ['spring_constant'] def __init__(self, index=-1, atom1_idx=None, atom2_idx=None, spring_constant=None, eq_dist=None): @@ -580,6 +638,17 @@ def __init__(self, index=-1, atom1_idx=None, atom2_idx=None, self._spring_constant = spring_constant self._eq_dist = eq_dist + def set_index(self, index): + """ + It sets the index of the bond. + + Parameters + ---------- + index : int + The index of this Bond object + """ + self._index = index + def set_atom1_idx(self, index): """ It sets atom1's index. @@ -671,6 +740,7 @@ class Angle(_TopologyElement): _name = 'Angle' _writable_attrs = ['atom1_idx', 'atom2_idx', 'atom3_idx', 'spring_constant', 'eq_angle'] + _lambda_changeable = ['spring_constant'] def __init__(self, index=-1, atom1_idx=None, atom2_idx=None, atom3_idx=None, spring_constant=None, eq_angle=None): @@ -699,6 +769,17 @@ def __init__(self, index=-1, atom1_idx=None, atom2_idx=None, self._spring_constant = spring_constant self._eq_angle = eq_angle + def set_index(self, index): + """ + It sets the index of the angle. + + Parameters + ---------- + index : int + The index of this Angle object + """ + self._index = index + def set_atom1_idx(self, index): """ It sets atom1's index. @@ -851,6 +932,17 @@ def __init__(self, index=-1, atom1_idx=None, atom2_idx=None, self._constant = constant self._phase = phase + def set_index(self, index): + """ + It sets the index of the dihedral. + + Parameters + ---------- + index : int + The index of this Dihedral object + """ + self._index = index + def set_atom1_idx(self, index): """ It sets atom1's index. @@ -1028,6 +1120,7 @@ class Proper(Dihedral): exclude = False _writable_attrs = ['atom1_idx', 'atom2_idx', 'atom3_idx', 'atom4_idx', 'constant', 'prefactor', 'periodicity', 'phase'] + _lambda_changeable = ['constant'] def include_in_14_list(self): """ @@ -1051,6 +1144,7 @@ class Improper(Dihedral): _name = 'Improper' _writable_attrs = ['atom1_idx', 'atom2_idx', 'atom3_idx', 'atom4_idx', 'constant', 'prefactor', 'periodicity'] + _lambda_changeable = ['constant'] class OFFDihedral(_TopologyElement): @@ -1061,6 +1155,7 @@ class OFFDihedral(_TopologyElement): _name = 'OFFDihedral' _writable_attrs = ['atom1_idx', 'atom2_idx', 'atom3_idx', 'atom4_idx', 'periodicity', 'phase', 'k', 'idivf'] + _lambda_changeable = ['k'] _to_PELE_class = Dihedral def __init__(self, index=-1, atom1_idx=None, atom2_idx=None, diff --git a/peleffy/topology/mapper.py b/peleffy/topology/mapper.py index 36c4a5a0..b10877a9 100644 --- a/peleffy/topology/mapper.py +++ b/peleffy/topology/mapper.py @@ -50,6 +50,26 @@ def __init__(self, molecule1, molecule2, include_hydrogens=True): self._molecule2 = molecule2 self._include_hydrogens = include_hydrogens + def get_mcs(self): + """ + It returns the Maximum Common Substructure (MCS) between + both molecules. + + Parameters + ---------- + mcs_mol : an RDKit.molecule object + The resulting MCS molecule + """ + from peleffy.utils.toolkits import RDKitToolkitWrapper + + rdkit_toolkit = RDKitToolkitWrapper() + + mcs_mol = rdkit_toolkit.get_mcs(self.molecule1, self.molecule2, + self._include_hydrogens, + self._TIMEOUT) + + return mcs_mol + def get_mapping(self): """ It returns the mapping between both molecules. @@ -60,14 +80,11 @@ def get_mapping(self): The list of atom pairs between both molecules, represented with tuples """ - from peleffy.utils.toolkits import RDKitToolkitWrapper rdkit_toolkit = RDKitToolkitWrapper() - mcs_mol = rdkit_toolkit.get_mcs(self.molecule1, self.molecule2, - self._include_hydrogens, - self._TIMEOUT) + mcs_mol = self.get_mcs() mapping = rdkit_toolkit.get_atom_mapping(self.molecule1, self.molecule2, diff --git a/peleffy/topology/rotamer.py b/peleffy/topology/rotamer.py index 47595e49..1062d89b 100644 --- a/peleffy/topology/rotamer.py +++ b/peleffy/topology/rotamer.py @@ -231,7 +231,7 @@ def __init__(self, molecule): Parameters ---------- - molecule : An peleffy.topology.Molecule + molecule : a peleffy.topology.Molecule A Molecule object to be written as an Impact file """ super().__init__(self) @@ -704,7 +704,7 @@ def __init__(self, molecule, atom_constraints): Parameters ---------- - molecule : An peleffy.topology.Molecule + molecule : a peleffy.topology.Molecule A Molecule object to be written as an Impact file atom_constraint : list[int or str] It defines the list of atoms to constrain in the core, thus, From 6ab2b52d1824cfee6a66f09b2976fb4c11edbadb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Sun, 26 Sep 2021 09:52:33 +0200 Subject: [PATCH 03/29] Fixes for alchemizer and more tests added --- peleffy/tests/test_alchemy.py | 982 ++++++++++++++++++++++++++++++++-- peleffy/topology/alchemy.py | 8 +- 2 files changed, 952 insertions(+), 38 deletions(-) diff --git a/peleffy/tests/test_alchemy.py b/peleffy/tests/test_alchemy.py index 9186373c..6845a948 100644 --- a/peleffy/tests/test_alchemy.py +++ b/peleffy/tests/test_alchemy.py @@ -70,8 +70,10 @@ def generate_molecules_and_topologies_from_smiles(smiles1, smiles2): from peleffy.topology import Molecule, Topology from peleffy.forcefield import OpenForceField - mol1 = Molecule(smiles=smiles1, hydrogens_are_explicit=False) - mol2 = Molecule(smiles=smiles2, hydrogens_are_explicit=False) + mol1 = Molecule(smiles=smiles1, hydrogens_are_explicit=False, + allow_undefined_stereo=True) + mol2 = Molecule(smiles=smiles2, hydrogens_are_explicit=False, + allow_undefined_stereo=True) openff = OpenForceField('openff_unconstrained-2.0.0.offxml') @@ -1695,20 +1697,477 @@ def test_vdw_lambda(self): assert improper_c2 - improper_c1 < 1e-5, \ 'Unexpected ratio between improper constants' + def test_bonded_lambda(self): + """ + It validates the effects of bonded lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import (WritableAtom, WritableBond, + WritableAngle, WritableProper, + WritableImproper) + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles('C=C', + 'C(Cl)(Cl)(Cl)') + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=0, + bonded_lambda=0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + bonded_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert (bond_sc2 / bond_sc1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert (angle_sc2 / angle_sc1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert (proper_c2 / proper_c1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert (improper_c2 / improper_c1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + bonded_lambda=1.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + bonded_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert (bond_sc2 / bond_sc1) - 0.2 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert (angle_sc2 / angle_sc1) - 0.2 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert (proper_c2 / proper_c1) - 0.2 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert (improper_c2 / improper_c1) - 0.2 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + bonded_lambda=0.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=1.0, + bonded_lambda=1.0) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert sigma2 - sigma1 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert epsilon2 - epsilon1 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert SASA_radius2 - SASA_radius1 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, " + "fep_lambda, coul1_lambda, coul2_lambda, " + "vdw_lambda, bonded_lambda, " + "golden_sigmas, golden_epsilons, " + "golden_born_radii, golden_SASA_radii, " + "golden_nonpolar_gammas, " + - "golden_nonpolar_alphas", + "golden_nonpolar_alphas, golden_charges", [(None, None, 'C=C', 'C(Cl)(Cl)(Cl)', 0.0, - None, - None, + None, + None, + None, + None, + [3.480646886945065, 3.480646886945065, + 2.5725815350632795, 2.5725815350632795, + 2.5725815350632795, 2.5725815350632795, 0.0], + [0.0868793154488, 0.0868793154488, + 0.01561134320353, 0.01561134320353, + 0.01561134320353, 0.01561134320353, 0.0], + [0, 0, 0, 0, 0, 0, 0], + [1.7403234434725325, 1.7403234434725325, + 1.2862907675316397, 1.2862907675316397, + 1.2862907675316397, 1.2862907675316397, 0.0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [-0.106311, -0.106311, 0.053156, 0.053156, + 0.053156, 0.053156, -0.0] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.2, + None, + None, + None, + None, + [3.480646886945065, 3.480646886945065, + 2.5725815350632795, 2.5725815350632795, + 2.0580652280506238, 2.0580652280506238, + 0.6615055612921249], + [0.0868793154488, 0.0868793154488, + 0.01561134320353, 0.01561134320353, + 0.012489074562824, + 0.012489074562824, 0.05312002093054], + [0, 0, 0, 0, 0, 0, 0], + [1.7403234434725325, 1.7403234434725325, + 1.2862907675316397, 1.2862907675316397, + 1.0290326140253119, 1.0290326140253119, + 0.33075278064606245], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [-0.106311, -0.106311, 0.053156, 0.053156, + 0.0425248, 0.0425248, -0.017483000000000002] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.8, + None, + None, + None, + None, + [3.480646886945065, 3.480646886945065, + 2.5725815350632795, 2.5725815350632795, + 0.5145163070126558, 0.5145163070126558, + 2.6460222451684996], + [0.0868793154488, 0.0868793154488, + 0.01561134320353, 0.01561134320353, + 0.003122268640705999, 0.003122268640705999, + 0.21248008372216], + [0, 0, 0, 0, 0, 0, 0], + [1.7403234434725325, 1.7403234434725325, + 1.2862907675316397, 1.2862907675316397, + 0.2572581535063279, 0.2572581535063279, + 1.3230111225842498], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [-0.106311, -0.106311, 0.053156, 0.053156, + 0.010631199999999999, 0.010631199999999999, + -0.06993200000000001] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 1.0, + None, + None, + None, + None, + [3.480646886945065, 3.480646886945065, + 2.5725815350632795, 2.5725815350632795, + 0.0, 0.0, + 3.3075278064606244], + [0.0868793154488, 0.0868793154488, + 0.01561134320353, 0.01561134320353, 0.0, 0.0, + 0.2656001046527], + [0, 0, 0, 0, 0, 0, 0], + [1.7403234434725325, 1.7403234434725325, + 1.2862907675316397, 1.2862907675316397, 0.0, + 0.0, 1.6537639032303122], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [-0.106311, -0.106311, 0.053156, 0.053156, + 0.0, 0.0, -0.087415] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.0, + 0.0, + 0.0, None, None, [3.480646886945065, 3.480646886945065, @@ -1716,71 +2175,73 @@ def test_vdw_lambda(self): 2.5725815350632795, 2.5725815350632795, 0.0], [0.0868793154488, 0.0868793154488, 0.01561134320353, 0.01561134320353, - 0.01561134320353, 0.01561134320353, 0.0], + 0.01561134320353, + 0.01561134320353, 0.0], [0, 0, 0, 0, 0, 0, 0], [1.7403234434725325, 1.7403234434725325, 1.2862907675316397, 1.2862907675316397, 1.2862907675316397, 1.2862907675316397, 0.0], [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0] + [0, 0, 0, 0, 0, 0, 0], + [-0.106311, -0.106311, 0.053156, 0.053156, + 0.053156, 0.053156, -0.0] ), (None, None, 'C=C', 'C(Cl)(Cl)(Cl)', - 0.2, - None, - None, + 0.0, + 1.0, + 0.0, None, None, [3.480646886945065, 3.480646886945065, 2.5725815350632795, 2.5725815350632795, - 2.0580652280506238, 2.0580652280506238, - 0.6615055612921249], + 2.5725815350632795, 2.5725815350632795, 0.0], [0.0868793154488, 0.0868793154488, 0.01561134320353, 0.01561134320353, - 0.012489074562824, - 0.012489074562824, 0.05312002093054], + 0.01561134320353, + 0.01561134320353, 0.0], [0, 0, 0, 0, 0, 0, 0], [1.7403234434725325, 1.7403234434725325, 1.2862907675316397, 1.2862907675316397, - 1.0290326140253119, 1.0290326140253119, - 0.33075278064606245], + 1.2862907675316397, 1.2862907675316397, 0.0], [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0] + [0, 0, 0, 0, 0, 0, 0], + [-0.106311, -0.106311, 0.053156, 0.053156, + 0.0, 0.0, -0.0] ), (None, None, 'C=C', 'C(Cl)(Cl)(Cl)', - 0.8, - None, - None, + 1.0, + 1.0, + 0.0, None, None, [3.480646886945065, 3.480646886945065, - 2.5725815350632795, 2.5725815350632795, - 0.5145163070126558, 0.5145163070126558, - 2.6460222451684996], + 2.5725815350632795, 2.5725815350632795, 0.0, 0.0, + 3.3075278064606244], [0.0868793154488, 0.0868793154488, - 0.01561134320353, 0.01561134320353, - 0.003122268640705999, 0.003122268640705999, - 0.21248008372216], + 0.01561134320353, 0.01561134320353, 0.0, 0.0, + 0.2656001046527], [0, 0, 0, 0, 0, 0, 0], [1.7403234434725325, 1.7403234434725325, - 1.2862907675316397, 1.2862907675316397, - 0.2572581535063279, 0.2572581535063279, - 1.3230111225842498], + 1.2862907675316397, 1.2862907675316397, 0.0, + 0.0, 1.6537639032303122], + [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0] + [-0.106311, -0.106311, 0.053156, 0.053156, + 0.0, 0.0, -0.0] ), (None, None, 'C=C', 'C(Cl)(Cl)(Cl)', 1.0, - None, - None, + 1.0, + 1.0, None, None, [3.480646886945065, 3.480646886945065, @@ -1795,7 +2256,9 @@ def test_vdw_lambda(self): 1.2862907675316397, 1.2862907675316397, 0.0, 0.0, 1.6537639032303122], [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0] + [0, 0, 0, 0, 0, 0, 0], + [-0.106311, -0.106311, 0.053156, 0.053156, + 0.0, 0.0, -0.087415] ), ]) def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, @@ -1807,7 +2270,8 @@ def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, golden_born_radii, golden_SASA_radii, golden_nonpolar_gammas, - golden_nonpolar_alphas): + golden_nonpolar_alphas, + golden_charges): """ It validates the effects of lambda on atom parameters. """ @@ -1849,3 +2313,453 @@ def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 'Unexpected non polar gammas' assert nonpolar_alphas == golden_nonpolar_alphas, \ 'Unexpected non polar alphas' + + @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, " + + "fep_lambda, coul1_lambda, coul2_lambda, " + + "vdw_lambda, bonded_lambda, " + + "golden_bond_spring_constants", + [(None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.0, + None, + None, + None, + None, + [399.1592953295, 397.2545789619, + 397.2545789619, 397.2545789619, + 397.2545789619, 172.40622182] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.5, + None, + None, + None, + None, + [399.1592953295, 397.2545789619, + 397.2545789619, 198.62728948095, + 198.62728948095, 172.40622182] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 1.0, + None, + None, + None, + None, + [399.1592953295, 397.2545789619, + 397.2545789619, 0.0, 0.0, 172.40622182] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + [399.1592953295, 397.2545789619, + 397.2545789619, 397.2545789619, + 397.2545789619, 172.40622182] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + [399.1592953295, 397.2545789619, + 397.2545789619, 0.0, 0.0, 172.40622182] + ) + ]) + def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, + fep_lambda, coul1_lambda, + coul2_lambda, vdw_lambda, + bonded_lambda, + golden_bond_spring_constants): + """ + It validates the effects of lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import WritableBond + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles(smiles1, smiles2) + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=fep_lambda, + coul1_lambda=coul1_lambda, + coul2_lambda=coul2_lambda, + vdw_lambda=vdw_lambda, + bonded_lambda=bonded_lambda) + + bond_spring_constants = list() + + for bond in top.bonds: + bond = WritableBond(bond) + bond_spring_constants.append(bond.spring_constant) + + assert bond_spring_constants == golden_bond_spring_constants, \ + 'Unexpected spring constants' + + @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, " + + "fep_lambda, coul1_lambda, coul2_lambda, " + + "vdw_lambda, bonded_lambda, " + + "golden_angle_spring_constants", + [(None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.0, + None, + None, + None, + None, + [34.066775159195, 34.066775159195, + 34.066775159195, 34.066775159195, + 22.69631544292, 22.69631544292, + 0.0, 0.0, 0.0] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.5, + None, + None, + None, + None, + [17.0333875795975, 17.0333875795975, + 34.066775159195, 34.066775159195, + 22.69631544292, 11.34815772146, + 26.602658132725, 26.602658132725, + 26.602658132725] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 1.0, + None, + None, + None, + None, + [0.0, 0.0, 34.066775159195, 34.066775159195, + 22.69631544292, 0.0, 53.20531626545, + 53.20531626545, 53.20531626545] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + [34.066775159195, 34.066775159195, + 34.066775159195, 34.066775159195, + 22.69631544292, 22.69631544292, + 0.0, 0.0, 0.0] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + [0.0, 0.0, 34.066775159195, 34.066775159195, + 22.69631544292, 0.0, 53.20531626545, + 53.20531626545, 53.20531626545] + ) + ]) + def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, + fep_lambda, coul1_lambda, + coul2_lambda, vdw_lambda, + bonded_lambda, + golden_angle_spring_constants): + """ + It validates the effects of lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import WritableAngle + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles(smiles1, smiles2) + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=fep_lambda, + coul1_lambda=coul1_lambda, + coul2_lambda=coul2_lambda, + vdw_lambda=vdw_lambda, + bonded_lambda=bonded_lambda) + + angle_spring_constants = list() + + for angle in top.angles: + angle = WritableAngle(angle) + angle_spring_constants.append(angle.spring_constant) + + assert angle_spring_constants == golden_angle_spring_constants, \ + 'Unexpected spring constants' + + @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, " + + "fep_lambda, coul1_lambda, coul2_lambda, " + + "vdw_lambda, bonded_lambda, " + + "golden_proper_constants, " + "golden_improper_constants", + [(None, + None, + 'C[N+](C)(C)CC(=O)[O-]', + '[NH]=C(N)c1ccccc1', + 0.0, + None, + None, + None, + None, + [0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, -0.3703352413219, + 0.02664938770063, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.4541676554336, 0.4541676554336, + 0.02664938770063, 0.02664938770063, + 0.1489710476446, 0.1489710476446, + 0.02960027280666, 0.02960027280666, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, + -0.0], + [10.5, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1] + ), + (None, + None, + 'C[N+](C)(C)CC(=O)[O-]', + '[NH]=C(N)c1ccccc1', + 0.5, + None, + None, + None, + None, + [0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, -0.18516762066095, + 0.013324693850315, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, + 0.2270838277168, 0.2270838277168, + 0.013324693850315, 0.013324693850315, + 0.0744855238223, 0.0744855238223, + 0.01480013640333, 0.01480013640333, + 0.4987082803621, 0.4987082803621, + 1.830965049538, 1.830965049538, + 1.830965049538, 1.830965049538, + 0.4987082803621, 0.4987082803621, + 3.368381238827, 0.9045236950015, + 0.9045236950015, 1.830965049538, + 1.830965049538, 1.830965049538, + 1.830965049538, 1.830965049538, + 1.830965049538, 1.830965049538, + 1.830965049538, 1.830965049538, + 1.830965049538, 1.830965049538, + 1.830965049538, 1.830965049538, + 1.830965049538, 1.830965049538, + 1.830965049538, 1.830965049538, + 1.830965049538, 1.830965049538, + 1.830965049538, -0.02408138896296, + -0.02408138896296], + [10.5, 1.1, 1.1, 0.55, 0.55, 0.55, 0.55] + ), + (None, + None, + 'C[N+](C)(C)CC(=O)[O-]', + '[NH]=C(N)c1ccccc1', + 1.0, + None, + None, + None, + None, + [0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -0.0, 0.0, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.9974165607242, 0.9974165607242, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 0.9974165607242, 0.9974165607242, + 6.736762477654, 1.809047390003, + 1.809047390003, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, -0.04816277792592, + -0.04816277792592], + [10.5, 1.1, 1.1, 0.0, 0.0, 0.0, 0.0] + ), + (None, + None, + 'C[N+](C)(C)CC(=O)[O-]', + '[NH]=C(N)c1ccccc1', + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + [0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, -0.3703352413219, + 0.02664938770063, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.4541676554336, 0.4541676554336, + 0.02664938770063, 0.02664938770063, + 0.1489710476446, 0.1489710476446, + 0.02960027280666, 0.02960027280666, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, + -0.0], + [10.5, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1] + ), + (None, + None, + 'C[N+](C)(C)CC(=O)[O-]', + '[NH]=C(N)c1ccccc1', + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + [0.06697375586735, 0.06697375586735, + 0.06697375586735, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -0.0, 0.0, 0.06697375586735, + 0.06697375586735, 0.06697375586735, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.9974165607242, 0.9974165607242, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 0.9974165607242, 0.9974165607242, + 6.736762477654, 1.809047390003, + 1.809047390003, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, -0.04816277792592, + -0.04816277792592], + [10.5, 1.1, 1.1, 0.0, 0.0, 0.0, 0.0] + ) + ]) + def test_dihedrals_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, + fep_lambda, coul1_lambda, + coul2_lambda, vdw_lambda, + bonded_lambda, + golden_proper_constants, + golden_improper_constants): + """ + It validates the effects of lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import WritableProper, WritableImproper + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles(smiles1, smiles2) + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=fep_lambda, + coul1_lambda=coul1_lambda, + coul2_lambda=coul2_lambda, + vdw_lambda=vdw_lambda, + bonded_lambda=bonded_lambda) + + proper_constants = list() + improper_constants = list() + + for proper in top.propers: + proper = WritableProper(proper) + proper_constants.append(proper.constant) + + for improper in top.impropers: + improper = WritableImproper(improper) + improper_constants.append(improper.constant) + + assert proper_constants == golden_proper_constants, \ + 'Unexpected proper constants' + assert improper_constants == golden_improper_constants, \ + 'Unexpected improper constants' diff --git a/peleffy/topology/alchemy.py b/peleffy/topology/alchemy.py index 5e2868fc..e094f6da 100644 --- a/peleffy/topology/alchemy.py +++ b/peleffy/topology/alchemy.py @@ -254,23 +254,23 @@ def topology_from_lambda_set(self, lambda_set): for proper_idx, proper in enumerate(alchemical_topology.propers): if proper_idx in self._exclusive_propers: - proper.apply_lambda(["spring_constant"], + proper.apply_lambda(["constant"], lambda_set.get_lambda_for_bonded(), reverse=False) if proper_idx in self._non_native_propers: - proper.apply_lambda(["spring_constant"], + proper.apply_lambda(["constant"], lambda_set.get_lambda_for_bonded(), reverse=True) for improper_idx, improper in enumerate(alchemical_topology.impropers): if improper_idx in self._exclusive_propers: - improper.apply_lambda(["spring_constant"], + improper.apply_lambda(["constant"], lambda_set.get_lambda_for_bonded(), reverse=False) if improper_idx in self._non_native_propers: - improper.apply_lambda(["spring_constant"], + improper.apply_lambda(["constant"], lambda_set.get_lambda_for_bonded(), reverse=True) From 18761a0d1b66398dd48e53f3fd8e7c393f869579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Sun, 26 Sep 2021 13:49:41 +0200 Subject: [PATCH 04/29] Add PDB writer for alchemical structures --- peleffy/tests/test_alchemy.py | 25 ++++++ peleffy/tests/test_topology.py | 29 +++++++ peleffy/topology/alchemy.py | 152 ++++++++++++++++++++++++++++++++- peleffy/topology/rotamer.py | 52 +++++++++++ peleffy/topology/topology.py | 8 ++ peleffy/utils/toolkits.py | 71 +++++++++++++++ 6 files changed, 334 insertions(+), 3 deletions(-) diff --git a/peleffy/tests/test_alchemy.py b/peleffy/tests/test_alchemy.py index 6845a948..ed776cbf 100644 --- a/peleffy/tests/test_alchemy.py +++ b/peleffy/tests/test_alchemy.py @@ -2763,3 +2763,28 @@ def test_dihedrals_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 'Unexpected proper constants' assert improper_constants == golden_improper_constants, \ 'Unexpected improper constants' + + def test_to_pdb(self): + """ + It validates the method to write the alchemical structure + to a PDB file. + """ + import os + import tempfile + from peleffy.utils import get_data_file_path, temporary_cd + from peleffy.topology import Alchemizer + from peleffy.tests.utils import compare_files + + mol1, mol2, top1, top2 = generate_molecules_and_topologies_from_pdb( + 'ligands/trimethylglycine.pdb', 'ligands/benzamidine.pdb') + + reference = get_data_file_path('tests/alchemical_structure.pdb') + + alchemizer = Alchemizer(top1, top2) + + with tempfile.TemporaryDirectory() as tmpdir: + with temporary_cd(tmpdir): + output_path = os.path.join(tmpdir, 'alchemical_structure.pdb') + alchemizer.to_pdb(output_path) + + compare_files(reference, output_path) diff --git a/peleffy/tests/test_topology.py b/peleffy/tests/test_topology.py index 4069ca80..b67024b6 100644 --- a/peleffy/tests/test_topology.py +++ b/peleffy/tests/test_topology.py @@ -2,6 +2,7 @@ This module contains the tests to check all available force fields in peleffy. """ +import pytest class TestTopology(object): @@ -476,3 +477,31 @@ def test_openffopls2005_parameterizer(self): expected_angles=expected_off_angles, expected_propers=expected_opls_propers, expected_impropers=expected_opls_impropers) + + def test_coreless_graph_error(self): + """ + It checks that the error when using a core-less molecular + graph is prompted. + """ + from peleffy.topology import Molecule + from peleffy.forcefield import OpenForceField + from peleffy.topology import Topology + from peleffy.utils import get_data_file_path + from peleffy.topology.rotamer import (MolecularGraph, + CoreLessMolecularGraph) + + pdb_path = get_data_file_path('ligands/ethylene.pdb') + molecule = Molecule(pdb_path) + openff = OpenForceField('openff_unconstrained-2.0.0.offxml') + parameters = openff.parameterize(molecule, charge_method='gasteiger') + + # Set a core-less molecular graph to molecule + molecule._graph = CoreLessMolecularGraph(molecule) + + with pytest.raises(TypeError): + _ = Topology(molecule, parameters) + + # With a regular molecular graph, it should not complain + molecule._graph = MolecularGraph(molecule) + + _ = Topology(molecule, parameters) diff --git a/peleffy/topology/alchemy.py b/peleffy/topology/alchemy.py index e094f6da..0b35ee1c 100644 --- a/peleffy/topology/alchemy.py +++ b/peleffy/topology/alchemy.py @@ -56,14 +56,20 @@ def __init__(self, topology1, topology2): # Join the two topologies self._joint_topology, self._non_native_atoms, \ self._non_native_bonds, self._non_native_angles, \ - self._non_native_propers, self._non_native_impropers = \ - self._join_topologies() + self._non_native_propers, self._non_native_impropers,\ + self._mol2_to_alc_map = self._join_topologies() # Get exclusive topological elements in topology 1 self._exclusive_atoms, self._exclusive_bonds, \ self._exclusive_angles, self._exclusive_propers, \ self._exclusive_impropers = self._get_exclusive_elements() + # Find connections + self._connections = self._find_connections() + + # Fix graph of molecule 2 + self._fix_molecule2_graph() + @property def topology1(self): """ @@ -138,6 +144,20 @@ def mcs_mol(self): """ return self._mcs_mol + @property + def connections(self): + """ + It returns the list of connections between molecule 1 and non + native atoms of molecule 2. + + Returns + ------- + connections : list[tuple[int, int]] + The list of connections between molecule 1 and non + native atoms of molecule 2 + """ + return self._connections + def get_alchemical_topology(self, fep_lambda=None, coul1_lambda=None, coul2_lambda=None, vdw_lambda=None, bonded_lambda=None): @@ -295,6 +315,9 @@ def _join_topologies(self): The list of proper indices that were added to topology 1 non_native_impropers : list[int] The list of improper indices that were added to topology 1 + mol2_to_alc_map : dict[int, int]] + The dictionary that pairs molecule 2 indices with alchemical + molecule indices """ from copy import deepcopy from peleffy.topology import Topology @@ -308,6 +331,9 @@ def _join_topologies(self): # First initialize the joint topology with topology 1 joint_topology = deepcopy(self.topology1) + # Change molecule tag + joint_topology.molecule.set_tag('HYB') + # Initialize list of non native topological elements non_native_atoms = list() non_native_bonds = list() @@ -475,7 +501,8 @@ def _join_topologies(self): joint_topology.add_improper(new_improper) return joint_topology, non_native_atoms, non_native_bonds, \ - non_native_angles, non_native_propers, non_native_impropers + non_native_angles, non_native_propers, non_native_impropers, \ + mol2_to_alc_map def _get_exclusive_elements(self): """ @@ -560,6 +587,125 @@ def _get_exclusive_elements(self): return exclusive_atoms, exclusive_bonds, exclusive_angles, \ exclusive_propers, exclusive_impropers + def _find_connections(self): + """ + It finds the connections between molecule 1 and molecule2. + + Returns + ------- + connections : list[tuple[int, int]] + The list of connections between molecule 1 and non + native atoms of molecule 2 + """ + # TODO move to rdkit toolkit + mol2_mapped_atoms = [atom_pair[1] for atom_pair in self.mapping] + + connections = list() + for atom in self.molecule2.rdkit_molecule.GetAtoms(): + if atom.GetIdx() in mol2_mapped_atoms: + for bond in atom.GetBonds(): + index1 = self._mol2_to_alc_map[bond.GetBeginAtomIdx()] + index2 = self._mol2_to_alc_map[bond.GetEndAtomIdx()] + if index1 in self._non_native_atoms: + connections.append((index1, index2)) + elif index2 in self._non_native_atoms: + connections.append((index1, index2)) + + return connections + + def _fix_molecule2_graph(self): + """ + If fixes the graph of molecule 2 to ensure that it has no core. + """ + from peleffy.topology.rotamer import CoreLessMolecularGraph + + self.molecule2._graph = CoreLessMolecularGraph(self.molecule2) + self.molecule2._rotamers = self.molecule2.graph.get_rotamers() + + def to_pdb(self, path): + """ + Writes the alchemical molecule to a PDB file. + + Parameters + ---------- + path : str + The path where to save the PDB file + """ + from copy import deepcopy + from peleffy.topology import Molecule + + from peleffy.utils.toolkits import RDKitToolkitWrapper + + rdkit_wrapper = RDKitToolkitWrapper() + + # Combine molecules + mol_combo = \ + rdkit_wrapper.alchemical_combination(self.molecule1.rdkit_molecule, + self.molecule2.rdkit_molecule, + self.mapping, + self.connections, + self.mcs_mol) + + # Generate a dummy peleffy Molecule with the required information + # to write it as a PDB file + molecule = Molecule() + molecule._rdkit_molecule = mol_combo + molecule.set_tag('HYB') + + rdkit_wrapper.to_pdb_file(molecule, path) + + def rotamer_library_to_file(self, path): + """ + It saves the alchemical rotamer library, which is the combination + of the rotamer libraries of both molecules, to the path that + is supplied. + + Parameters + ---------- + path : str + The path where to save the rotamer library + """ + # TODO build common graph and it should depend on the lambda too --> some bonds might be rotatable after the alchemical change + # TODO maybe PELE can accept multiple rotamer branches with duplicated atoms (unlikely but we should try it) + # Initial definitions + pdb_atom_names = [atom.PDB_name.replace(' ', '_',) + for atom in self._joint_topology.atoms] + molecule_tag = self._joint_topology.molecule.tag + mol2_mapped_atoms = [atom_pair[1] for atom_pair in self.mapping] + + with open(path, 'w') as file: + file.write('rot assign res {} &\n'.format(molecule_tag)) + for i, rotamer_branches in enumerate(self.molecule1.rotamers): + if i > 0: + file.write(' newgrp &\n') + for rotamer in rotamer_branches: + index1 = rotamer.index1 + index2 = rotamer.index2 + atom_name1 = pdb_atom_names[index1] + atom_name2 = pdb_atom_names[index2] + file.write(' sidelib FREE{} {} {} &\n'.format( + rotamer.resolution, atom_name1, atom_name2)) + for rotamer_branches in self.molecule2.rotamers: + file.write(' newgrp &\n') + for rotamer in rotamer_branches: + index1 = rotamer.index1 + index2 = rotamer.index2 + # We need to prevent adding atoms that have been already added + if (index1 in mol2_mapped_atoms and + index2 in mol2_mapped_atoms): + continue + + if index1 in self._non_native_atoms: + index1 = self._mol2_to_alc_map[index1] + + if index2 in self._non_native_atoms: + index2 = self._mol2_to_alc_map[index2] + + atom_name1 = pdb_atom_names[index1] + atom_name2 = pdb_atom_names[index2] + file.write(' sidelib FREE{} {} {} &\n'.format( + rotamer.resolution, atom_name1, atom_name2)) + class Lambda(ABC): """ diff --git a/peleffy/topology/rotamer.py b/peleffy/topology/rotamer.py index 1062d89b..9fcc7a46 100644 --- a/peleffy/topology/rotamer.py +++ b/peleffy/topology/rotamer.py @@ -819,3 +819,55 @@ def constraint_names(self): constraint_names.append(atom_names[index]) return constraint_names + + +class CoreLessMolecularGraph(MolecularGraph): + """ + It represents the structure of a Molecule as a networkx.Graph with + an empty atom core. This molecular graph is only valid for + alchemy applications. + """ + + def __init__(self, molecule): + """ + It initializes a CoreLessMolecularGraph object. + + Parameters + ---------- + molecule : a peleffy.topology.Molecule + A Molecule object to be written as an Impact file + """ + nx.Graph.__init__(self) + self._molecule = molecule + self._compute_rotamer_graph() + self._core_nodes = [] + + def get_rotamers(self): + """ + It builds the RotamerLibrary object. + + Returns + ------- + rotamers : list[list] + The list of rotamers grouped by the branch they belong to + """ + resolution = self.molecule.rotamer_resolution + + branch_graph = deepcopy(self) + + branch_groups = list(nx.connected_components(branch_graph)) + + rot_bonds_per_group = self._get_rot_bonds_per_group(branch_groups) + + rotamers = list() + + for group_id, rot_bonds in enumerate(rot_bonds_per_group): + branch_rotamers = list() + for (atom1_index, atom2_index) in rot_bonds: + rotamer = Rotamer(atom1_index, atom2_index, resolution) + branch_rotamers.append(rotamer) + + if len(branch_rotamers) > 0: + rotamers.append(branch_rotamers) + + return rotamers diff --git a/peleffy/topology/topology.py b/peleffy/topology/topology.py index c94729d4..8c4c8708 100644 --- a/peleffy/topology/topology.py +++ b/peleffy/topology/topology.py @@ -101,6 +101,7 @@ def _build_atoms(self): """It builds the atoms of the molecule.""" from peleffy.utils import Logger + from peleffy.topology.rotamer import CoreLessMolecularGraph logger = Logger() coords = RDKitToolkitWrapper().get_coordinates(self.molecule) @@ -123,6 +124,13 @@ def _build_atoms(self): nonpolar_alpha=alpha) self.add_atom(atom) + if isinstance(self.molecule.graph, CoreLessMolecularGraph): + message = f'Error: core-less molecular graph cannot be ' + \ + f'used to build a non-alchemic topology for ' + \ + f'molecule {self.molecule.name}' + logger.error([message]) + raise TypeError(message) + for atom in self.atoms: if atom.index in self.molecule.graph.core_nodes: atom.set_as_core() diff --git a/peleffy/utils/toolkits.py b/peleffy/utils/toolkits.py index 53b618a9..0524580f 100644 --- a/peleffy/utils/toolkits.py +++ b/peleffy/utils/toolkits.py @@ -853,6 +853,77 @@ def draw_mapping(self, molecule1, molecule2, mcs_mol, return image + def alchemical_combination(self, mol1, mol2, atom_mapping, + connections, mcs_mol): + """ + Given two molecules, it return the alchemical combination of + them. Both molecules are superposed taking the first one + as the reference. + + Parameters + ---------- + mol1 : an RDKit.molecule object + The first molecule to combine. It will be used as reference + during the superposition + mol2 : an RDKit.molecule object + The second molecule to combine + atom_mapping : list[tuple[int, int]] + The list containing the mapping between atoms of both + molecules. First index of each pair belongs to molecule + 1, the second one belongs to molecule 2 + connections : list[tuple[int, int]] + The list of connections between molecule 1 and non + native atoms of molecule 2 + mcs_mol : an RDKit.molecule object + The molecule representinc the MCS substructure of both + molecules + + Returns + ------- + mol_combo : an RDKit.molecule object + The resulting molecule after combining both supplied + molecules + """ + from rdkit import Chem + from copy import deepcopy + + # Make a copy of molecule 2 + mol2 = deepcopy(mol2) + + # Generate inverse mapping + inverse_mapping = [(idxs[1], idxs[0]) for idxs in atom_mapping] + + # Align molecule 2 to molecule 1 + Chem.rdMolAlign.AlignMol(mol2, mol1, + atomMap=inverse_mapping) + + # Remove common substructure from molecule 2 + mol2_truncated = Chem.DeleteSubstructs(mol2, mcs_mol) + + # Combine molecule1 with truncated molecule 2 + mol_combo = Chem.CombineMols(mol1, + mol2_truncated) + + # Editable molecule + mol_combo = Chem.EditableMol(mol_combo) + for connection in connections: + mol_combo.AddBond(*connection) + + mol_combo = mol_combo.GetMol() + + # Set PDB information + pdb_info = Chem.AtomPDBResidueInfo() + pdb_info.SetResidueName('HYB') + pdb_info.SetResidueNumber(1) + pdb_info.SetChainId('L') + pdb_info.SetIsHeteroAtom(True) + for atom in mol_combo.GetAtoms(): + atom_name = atom.GetPDBResidueInfo().GetName() + pdb_info.SetName(atom_name) + atom.SetPDBResidueInfo(pdb_info) + + return mol_combo + class AmberToolkitWrapper(ToolkitWrapper): """ From 67a16b5923b020756b82a3e9113878bc00dcee41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Sun, 26 Sep 2021 13:50:10 +0200 Subject: [PATCH 05/29] Add alchemical structure for testing --- peleffy/data/tests/alchemical_structure.pdb | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 peleffy/data/tests/alchemical_structure.pdb diff --git a/peleffy/data/tests/alchemical_structure.pdb b/peleffy/data/tests/alchemical_structure.pdb new file mode 100644 index 00000000..2444dbca --- /dev/null +++ b/peleffy/data/tests/alchemical_structure.pdb @@ -0,0 +1,44 @@ +HETATM 1 C1 HYB L 1 -0.071 -1.459 -0.304 1.00 0.00 C +HETATM 2 N1 HYB L 1 -0.237 -0.057 -0.095 1.00 0.00 N1+ +HETATM 3 C2 HYB L 1 -1.150 0.412 -1.153 1.00 0.00 C +HETATM 4 C3 HYB L 1 -0.923 0.191 1.157 1.00 0.00 C +HETATM 5 C4 HYB L 1 0.942 0.732 -0.249 1.00 0.00 C +HETATM 6 C5 HYB L 1 2.046 0.408 0.633 1.00 0.00 C +HETATM 7 O1 HYB L 1 3.116 1.052 0.564 1.00 0.00 O +HETATM 8 O2 HYB L 1 1.991 -0.597 1.577 1.00 0.00 O1- +HETATM 9 H1 HYB L 1 -0.318 -1.760 -1.343 1.00 0.00 H +HETATM 10 H2 HYB L 1 -0.821 -1.989 0.347 1.00 0.00 H +HETATM 11 H3 HYB L 1 0.903 -1.865 -0.059 1.00 0.00 H +HETATM 12 H4 HYB L 1 -0.567 0.400 -2.104 1.00 0.00 H +HETATM 13 H5 HYB L 1 -2.067 -0.176 -1.186 1.00 0.00 H +HETATM 14 H6 HYB L 1 -1.369 1.481 -0.953 1.00 0.00 H +HETATM 15 H7 HYB L 1 -1.903 0.672 1.034 1.00 0.00 H +HETATM 16 H8 HYB L 1 -1.108 -0.781 1.667 1.00 0.00 H +HETATM 17 H9 HYB L 1 -0.323 0.825 1.846 1.00 0.00 H +HETATM 18 H10 HYB L 1 1.237 0.709 -1.316 1.00 0.00 H +HETATM 19 H11 HYB L 1 0.622 1.801 -0.063 1.00 0.00 H +HETATM 20 C1 HYB L 1 -0.779 0.348 1.160 1.00 0.00 C +HETATM 21 C2 HYB L 1 -0.825 -0.520 2.218 1.00 0.00 C +HETATM 22 C3 HYB L 1 -0.996 -0.028 3.518 1.00 0.00 C +HETATM 23 C4 HYB L 1 -1.127 1.352 3.741 1.00 0.00 C +HETATM 24 C5 HYB L 1 -1.087 2.225 2.651 1.00 0.00 C +HETATM 25 C6 HYB L 1 -0.914 1.710 1.369 1.00 0.00 C +HETATM 26 H2 HYB L 1 -0.729 -1.582 2.050 1.00 0.00 H +HETATM 27 H3 HYB L 1 -1.027 -0.710 4.355 1.00 0.00 H +HETATM 28 H4 HYB L 1 -1.256 1.727 4.746 1.00 0.00 H +HETATM 29 H5 HYB L 1 -1.191 3.289 2.807 1.00 0.00 H +HETATM 30 H6 HYB L 1 -0.885 2.390 0.531 1.00 0.00 H +CONECT 1 2 9 10 11 +CONECT 2 3 4 5 20 +CONECT 3 12 13 14 +CONECT 4 15 16 17 +CONECT 5 6 18 19 +CONECT 6 7 7 8 +CONECT 20 21 21 25 +CONECT 21 22 26 +CONECT 22 23 23 27 +CONECT 23 24 28 +CONECT 24 25 25 29 +CONECT 25 30 +END + From ad87ff6186a7545bfa90e12c2f77326b0aae2def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Sun, 26 Sep 2021 18:00:28 +0200 Subject: [PATCH 06/29] Add alchemical graph --- peleffy/tests/test_alchemy.py | 25 +++++ peleffy/topology/alchemy.py | 198 ++++++++++++++++++++++++++++------ peleffy/topology/rotamer.py | 2 +- 3 files changed, 193 insertions(+), 32 deletions(-) diff --git a/peleffy/tests/test_alchemy.py b/peleffy/tests/test_alchemy.py index ed776cbf..3f6cfea0 100644 --- a/peleffy/tests/test_alchemy.py +++ b/peleffy/tests/test_alchemy.py @@ -2788,3 +2788,28 @@ def test_to_pdb(self): alchemizer.to_pdb(output_path) compare_files(reference, output_path) + + def test_rotamer_library_to_file(self): + """ + It validates the method to write the alchemical rotamer + library. + """ + import os + import tempfile + from peleffy.utils import get_data_file_path, temporary_cd + from peleffy.topology import Alchemizer + from peleffy.tests.utils import compare_files + + mol1, mol2, top1, top2 = generate_molecules_and_topologies_from_pdb( + 'ligands/trimethylglycine.pdb', 'ligands/benzamidine.pdb') + + reference = get_data_file_path('tests/alchemical_structure.pdb') + + alchemizer = Alchemizer(top1, top2) + + with tempfile.TemporaryDirectory() as tmpdir: + with temporary_cd(tmpdir): + output_path = os.path.join(tmpdir, 'alchemical_structure.pdb') + alchemizer.to_pdb(output_path) + + compare_files(reference, output_path) diff --git a/peleffy/topology/alchemy.py b/peleffy/topology/alchemy.py index 0b35ee1c..41bc6fd7 100644 --- a/peleffy/topology/alchemy.py +++ b/peleffy/topology/alchemy.py @@ -47,11 +47,11 @@ def __init__(self, topology1, topology2): from peleffy.topology import Mapper # Map atoms from both molecules - mapper = Mapper(self.molecule1, self.molecule2, - include_hydrogens=True) + self._mapper = Mapper(self.molecule1, self.molecule2, + include_hydrogens=True) - self._mapping = mapper.get_mapping() - self._mcs_mol = mapper.get_mcs() + self._mapping = self._mapper.get_mapping() + self._mcs_mol = self._mapper.get_mcs() # Join the two topologies self._joint_topology, self._non_native_atoms, \ @@ -67,8 +67,8 @@ def __init__(self, topology1, topology2): # Find connections self._connections = self._find_connections() - # Fix graph of molecule 2 - self._fix_molecule2_graph() + # Generate alchemical graph + self._graph, self._rotamers = self._generate_alchemical_graph() @property def topology1(self): @@ -613,14 +613,92 @@ def _find_connections(self): return connections - def _fix_molecule2_graph(self): + def _generate_alchemical_graph(self): """ - If fixes the graph of molecule 2 to ensure that it has no core. + If generates the alchemical graph and rotamers. + + Returns + ------- + alchemical_graph : a peleffy.topology.rotamer.MolecularGraph object + The molecular graph containing the alchemical structure + rotamers : list[list] + The list of rotamers grouped by the branch they belong to """ - from peleffy.topology.rotamer import CoreLessMolecularGraph + from copy import deepcopy - self.molecule2._graph = CoreLessMolecularGraph(self.molecule2) - self.molecule2._rotamers = self.molecule2.graph.get_rotamers() + # Define mappers + mol1_mapped_atoms = [atom_pair[0] for atom_pair in self.mapping] + mol2_mapped_atoms = [atom_pair[1] for atom_pair in self.mapping] + mol2_to_mol1_map = dict(zip(mol2_mapped_atoms, mol1_mapped_atoms)) + + # Copy graph of molecule 1 + alchemical_graph = deepcopy(self.molecule1._graph) + + # Fix conflicts on common edges of both molecules + for mol2_edge in self.molecule2.graph.edges: + if (mol2_edge[0] in mol2_to_mol1_map.keys() and + mol2_edge[1] in mol2_to_mol1_map.keys()): + # Get indices of both atoms of this edge + index1 = mol2_to_mol1_map[mol2_edge[0]] + index2 = mol2_to_mol1_map[mol2_edge[1]] + + # Get weights in each graph + weight1 = alchemical_graph[index1][index2]['weight'] + weight2 = \ + self.molecule2.graph[mol2_edge[0]][mol2_edge[1]]['weight'] + + # We keep the higher weight, so in case that a bond is + # rotatable in one molecule but not in the order + # one, it will be defined as rotatable + weight = int(max((weight1, weight2))) + alchemical_graph[index1][index2]['weight'] = weight + + # Update nrot_neighbors list + node1 = alchemical_graph.nodes[index1] + node2 = alchemical_graph.nodes[index2] + if weight == 1: # Rotatable + if index2 in node1['nrot_neighbors']: + node1['nrot_neighbors'].remove(index2) + if index1 in node2['nrot_neighbors']: + node2['nrot_neighbors'].remove(index1) + else: # Non rotatable + if index2 not in node1['nrot_neighbors']: + node1['nrot_neighbors'].append(index2) + if index1 not in node2['nrot_neighbors']: + node2['nrot_neighbors'].append(index1) + + # Add non native nodes + for mol2_node1 in self.molecule2.graph.nodes: + index1 = self._mol2_to_alc_map[mol2_node1] + if index1 in self._non_native_atoms: + name = self.molecule2.graph.nodes[mol2_node1]['pdb_name'] + nrot_neighbors = \ + self.molecule2.graph.nodes[mol2_node1]['nrot_neighbors'] + nrot_neighbors = [self._mol2_to_alc_map[neighbor] + for neighbor in nrot_neighbors] + alchemical_graph.add_node(index1, pdb_name=name, + nrot_neighbors=nrot_neighbors) + + for mol2_node2 in self.molecule2.graph[mol2_node1]: + index2 = self._mol2_to_alc_map[mol2_node2] + if not alchemical_graph.has_edge(index1, index2): + rotatable = \ + self.molecule2.graph[mol2_node1][mol2_node2]['weight'] + alchemical_graph.add_edge(index1, index2, + weight=int(rotatable)) + + alchemical_graph._build_core_nodes() + + rotamers = alchemical_graph.get_rotamers() + + # Update core/branch location + for atom in self._joint_topology.atoms: + if atom.index in alchemical_graph.core_nodes: + atom.set_as_core() + else: + atom.set_as_branch() + + return alchemical_graph, rotamers def to_pdb(self, path): """ @@ -654,19 +732,79 @@ def to_pdb(self, path): rdkit_wrapper.to_pdb_file(molecule, path) - def rotamer_library_to_file(self, path): + def rotamer_library_to_file(self, path, fep_lambda=None, + coul1_lambda=None, coul2_lambda=None, + vdw_lambda=None, bonded_lambda=None): """ It saves the alchemical rotamer library, which is the combination of the rotamer libraries of both molecules, to the path that is supplied. + Returns + ------- + fep_lambda : float + The value to define an FEP lambda. This lambda affects + all the parameters. It needs to be contained between + 0 and 1. Default is None + coul1_lambda : float + The value to define a coulombic lambda for exclusive atoms + of molecule 1. This lambda only affects coulombic parameters + of exclusive atoms of molecule 1. It needs to be contained + between 0 and 1. Default is None + coul2_lambda : float + The value to define a coulombic lambda for exclusive atoms + of molecule 2. This lambda only affects coulombic parameters + of exclusive atoms of molecule 2. It needs to be contained + between 0 and 1. Default is None + vdw_lambda : float + The value to define a vdw lambda. This lambda only + affects van der Waals parameters. It needs to be contained + between 0 and 1. Default is None + bonded_lambda : float + The value to define a coulombic lambda. This lambda only + affects bonded parameters. It needs to be contained + between 0 and 1. Default is None + Parameters ---------- path : str The path where to save the rotamer library """ - # TODO build common graph and it should depend on the lambda too --> some bonds might be rotatable after the alchemical change - # TODO maybe PELE can accept multiple rotamer branches with duplicated atoms (unlikely but we should try it) + + at_least_one = fep_lambda is not None or \ + coul1_lambda is not None or coul2_lambda is not None or \ + vdw_lambda is not None or bonded_lambda is not None + + # Define lambdas + fep_lambda = FEPLambda(fep_lambda) + coul1_lambda = Coulombic1Lambda(coul1_lambda) + coul2_lambda = Coulombic2Lambda(coul2_lambda) + vdw_lambda = VanDerWaalsLambda(vdw_lambda) + bonded_lambda = BondedLambda(bonded_lambda) + + lambda_set = LambdaSet(fep_lambda, coul1_lambda, coul2_lambda, + vdw_lambda, bonded_lambda) + + if (at_least_one and + lambda_set.get_lambda_for_bonded() == 0.0 and + lambda_set.get_lambda_for_vdw() == 0.0 and + lambda_set.get_lambda_for_coulomb1() == 0.0 and + lambda_set.get_lambda_for_coulomb2() == 0.0): + rotamers = self.molecule1.rotamers + mapping = False + + elif (at_least_one and + lambda_set.get_lambda_for_bonded() == 1.0 and + lambda_set.get_lambda_for_vdw() == 1.0 and + lambda_set.get_lambda_for_coulomb1() == 1.0 and + lambda_set.get_lambda_for_coulomb2() == 1.0): + rotamers = self.molecule2.rotamers + mapping = True + + else: + rotamers = self._rotamers + mapping = False + # Initial definitions pdb_atom_names = [atom.PDB_name.replace(' ', '_',) for atom in self._joint_topology.atoms] @@ -675,30 +813,15 @@ def rotamer_library_to_file(self, path): with open(path, 'w') as file: file.write('rot assign res {} &\n'.format(molecule_tag)) - for i, rotamer_branches in enumerate(self.molecule1.rotamers): + for i, rotamer_branches in enumerate(rotamers): if i > 0: file.write(' newgrp &\n') for rotamer in rotamer_branches: index1 = rotamer.index1 index2 = rotamer.index2 - atom_name1 = pdb_atom_names[index1] - atom_name2 = pdb_atom_names[index2] - file.write(' sidelib FREE{} {} {} &\n'.format( - rotamer.resolution, atom_name1, atom_name2)) - for rotamer_branches in self.molecule2.rotamers: - file.write(' newgrp &\n') - for rotamer in rotamer_branches: - index1 = rotamer.index1 - index2 = rotamer.index2 - # We need to prevent adding atoms that have been already added - if (index1 in mol2_mapped_atoms and - index2 in mol2_mapped_atoms): - continue - if index1 in self._non_native_atoms: + if mapping: index1 = self._mol2_to_alc_map[index1] - - if index2 in self._non_native_atoms: index2 = self._mol2_to_alc_map[index2] atom_name1 = pdb_atom_names[index1] @@ -706,6 +829,19 @@ def rotamer_library_to_file(self, path): file.write(' sidelib FREE{} {} {} &\n'.format( rotamer.resolution, atom_name1, atom_name2)) + def _ipython_display_(self): + """ + It returns a representation of the alchemical mapping. + + Returns + ------- + mapping_representation : a IPython display object + Displayable RDKit molecules with mapping information + """ + from IPython.display import display + + return display(self._mapper) + class Lambda(ABC): """ diff --git a/peleffy/topology/rotamer.py b/peleffy/topology/rotamer.py index 9fcc7a46..82589215 100644 --- a/peleffy/topology/rotamer.py +++ b/peleffy/topology/rotamer.py @@ -241,7 +241,7 @@ def __init__(self, molecule): def _compute_rotamer_graph(self): """ - It initializes the netwrokx.Graph with a Molecule object. + It initializes the network.Graph with a Molecule object. """ rdkit_toolkit = RDKitToolkitWrapper() rot_bonds_atom_ids = \ From 12565078f0a40f87a9b2121d1e2154588c8b687e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Sun, 26 Sep 2021 18:09:47 +0200 Subject: [PATCH 07/29] Remove unused module dataclasses --- peleffy/topology/alchemy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/peleffy/topology/alchemy.py b/peleffy/topology/alchemy.py index 41bc6fd7..d5855087 100644 --- a/peleffy/topology/alchemy.py +++ b/peleffy/topology/alchemy.py @@ -4,7 +4,6 @@ from abc import ABC -from dataclasses import dataclass class Alchemizer(object): From ed8ff4cee0b6226691700fc9fb10a40de273298d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 29 Sep 2021 13:00:00 +0200 Subject: [PATCH 08/29] Modify mapped topological elements --- .../{test_alchemy.py => test_alchemistry.py} | 1423 +++++++++++++---- peleffy/topology/__init__.py | 2 +- .../topology/{alchemy.py => alchemistry.py} | 300 +++- peleffy/topology/elements.py | 28 +- peleffy/utils/toolkits.py | 64 +- 5 files changed, 1459 insertions(+), 358 deletions(-) rename peleffy/tests/{test_alchemy.py => test_alchemistry.py} (69%) rename peleffy/topology/{alchemy.py => alchemistry.py} (77%) diff --git a/peleffy/tests/test_alchemy.py b/peleffy/tests/test_alchemistry.py similarity index 69% rename from peleffy/tests/test_alchemy.py rename to peleffy/tests/test_alchemistry.py index 3f6cfea0..af226fda 100644 --- a/peleffy/tests/test_alchemy.py +++ b/peleffy/tests/test_alchemistry.py @@ -1,5 +1,5 @@ """ -This module contains tests that check that the alchemy module. +This module contains tests that check that the alchemistry module. """ import pytest @@ -86,8 +86,8 @@ def generate_molecules_and_topologies_from_smiles(smiles1, smiles2): return mol1, mol2, top1, top2 -class TestAlchemy(object): - """Alchemy test.""" +class TestAlchemistry(object): + """Alchemistry test.""" def test_alchemizer_initialization_checker(self): """ @@ -119,7 +119,7 @@ def test_alchemizer_initialization_checker(self): 'C(Cl)(Cl)(Cl)', [(0, 0), (1, 1), (2, 2), (3, 4)], [6, ], - [], + [5, ], [6, 7, 8], [], [], @@ -137,7 +137,7 @@ def test_alchemizer_initialization_checker(self): (5, 5), (11, 6), (10, 11), (9, 10), (8, 9), (7, 8), (6, 7)], [12, 13, 14], - [], + [12, 13, 14], [18, 19, 20, 21, 22, 23], [], [], @@ -171,7 +171,7 @@ def test_alchemizer_initialization_checker(self): None, [(0, 0), (1, 1), (3, 4), (2, 2)], [4, 5], - [], + [3, 4], [2, 3, 4, 5], [1, 2], [], @@ -187,8 +187,8 @@ def test_alchemizer_initialization_checker(self): None, [(0, 5), (1, 0), (2, 6), (3, 1), (4, 2), (5, 4), (9, 10), (6, 3), (7, 8), (8, 9)], - [10], - [], + [10, ], + [9, ], [13, 14, 15], [23, 24], [], @@ -224,7 +224,8 @@ def test_alchemizer_initialization_checker(self): [(1, 6), (0, 7), (8, 14), (9, 15), (2, 8), (11, 16)], [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], - [], + [18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29], [33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52], [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, @@ -387,7 +388,7 @@ def test_fep_lambda(self): 'Unexpected ratio between SASA radii' for charge1, charge2 in zip(charges1, charges2): - assert (charge2 / charge1) - (1- 0.2) < 1e-5, \ + assert (charge2 / charge1) - (1 - 0.2) < 1e-5, \ 'Unexpected ratio between charges' for bond_sc1, bond_sc2 in zip(bond_spring_constants1, @@ -558,7 +559,7 @@ def test_fep_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants1.append(improper.constant) - top = alchemizer.get_alchemical_topology(fep_lambda=1.0) + top = alchemizer.get_alchemical_topology(fep_lambda=0.5) sigmas2 = list() epsilons2 = list() @@ -602,45 +603,104 @@ def test_fep_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants2.append(improper.constant) - for sigma1, sigma2 in zip(sigmas1, sigmas2): - assert sigma2 - sigma1 < 1e-5, \ + top = alchemizer.get_alchemical_topology(fep_lambda=1.0) + + sigmas3 = list() + epsilons3 = list() + SASA_radii3 = list() + charges3 = list() + bond_spring_constants3 = list() + angle_spring_constants3 = list() + proper_constants3 = list() + improper_constants3 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas3.append(atom.sigma) + epsilons3.append(atom.epsilon) + SASA_radii3.append(atom.SASA_radius) + charges3.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants3.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants3.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants3.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants3.append(improper.constant) + + for sigma1, sigma2, sigma3 in zip(sigmas1, sigmas2, sigmas3): + assert sigma1 / sigma2 - sigma2 / sigma3 < 1e-5, \ 'Unexpected ratio between sigmas' - for epsilon1, epsilon2 in zip(epsilons1, epsilons2): - assert epsilon2 - epsilon1 < 1e-5, \ + for epsilon1, epsilon2, epsilon3 in zip(epsilons1, epsilons2, epsilons3): + assert epsilon1 / epsilon2 - epsilon2 / epsilon3 < 1e-5, \ 'Unexpected ratio between epsilons' + assert abs(epsilon1 - epsilon2) > 1e-5, \ + 'Unexpected invariant epsilons' - for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): - assert SASA_radius2 - SASA_radius1 < 1e-5, \ + for radius1, radius2, radius3 in zip(SASA_radii1, SASA_radii2, + SASA_radii3): + assert radius1 / radius2 - radius2 / radius3 < 1e-5, \ 'Unexpected ratio between SASA radii' + assert abs(radius1 - radius2) > 1e-5, \ + 'Unexpected invariant SASA radii' - for charge1, charge2 in zip(charges1, charges2): - assert charge2 - charge1 < 1e-5, \ + for charge1, charge2, charge3 in zip(charges1, charges2, charges3): + assert charge1 / charge2 - charge2 / charge3 < 1e-5, \ 'Unexpected ratio between charges' + assert abs(charge1 - charge2) > 1e-5, \ + 'Unexpected invariant charges' - for bond_sc1, bond_sc2 in zip(bond_spring_constants1, - bond_spring_constants2): - assert bond_sc2 - bond_sc1 < 1e-5, \ + for bond_sc1, bond_sc2, bond_sc3 in zip(bond_spring_constants1, + bond_spring_constants2, + bond_spring_constants3): + assert bond_sc1 / bond_sc2 - bond_sc2 / bond_sc3 < 1e-5, \ 'Unexpected ratio between bond spring constants' + assert abs(bond_sc1 - bond_sc2) > 1e-5, \ + 'Unexpected invariant bond spring constants' - for angle_sc1, angle_sc2 in zip(angle_spring_constants1, - angle_spring_constants2): - assert angle_sc2 - angle_sc1 < 1e-5, \ + for angle_sc1, angle_sc2, angle_sc3 in zip(angle_spring_constants1, + angle_spring_constants2, + angle_spring_constants3): + assert angle_sc1 / angle_sc2 - angle_sc2 / angle_sc3 < 1e-5, \ 'Unexpected ratio between angle spring constants' + assert abs(angle_sc1 - angle_sc2) > 1e-5, \ + 'Unexpected invariant angle spring constants' - for proper_c1, proper_c2 in zip(proper_constants1, - proper_constants2): - assert proper_c2 - proper_c1 < 1e-5, \ + for proper_c1, proper_c2, proper_c3 in zip(proper_constants1, + proper_constants2, + proper_constants3): + assert proper_c1 / proper_c2 - proper_c2 / proper_c3 < 1e-5, \ 'Unexpected ratio between proper constants' - for improper_c1, improper_c2 in zip(improper_constants1, - improper_constants2): - assert improper_c2 - improper_c1 < 1e-5, \ + for improper_c1, improper_c2, improper_c3 in zip(improper_constants1, + improper_constants2, + improper_constants3): + assert improper_c1 / improper_c2 - improper_c2 / improper_c3 < 1e-5, \ 'Unexpected ratio between improper constants' - def test_coul1_lambda(self): + def test_coul_lambda(self): """ - It validates the effects of coul1 lambda on atom parameters. + It validates the effects of coul lambda on atom parameters. """ from peleffy.topology import Alchemizer from peleffy.template.impact import (WritableAtom, WritableBond, @@ -654,7 +714,7 @@ def test_coul1_lambda(self): alchemizer = Alchemizer(top1, top2) top = alchemizer.get_alchemical_topology(fep_lambda=0, - coul1_lambda=0) + coul_lambda=0) sigmas1 = list() epsilons1 = list() @@ -689,7 +749,7 @@ def test_coul1_lambda(self): improper_constants1.append(improper.spring_constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - coul1_lambda=0.2) + coul_lambda=0.2) sigmas2 = list() epsilons2 = list() @@ -760,7 +820,7 @@ def test_coul1_lambda(self): 'Unexpected ratio between improper constants' top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - coul1_lambda=0.0) + coul_lambda=1.0) sigmas1 = list() epsilons1 = list() @@ -795,7 +855,7 @@ def test_coul1_lambda(self): improper_constants1.append(improper.spring_constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - coul1_lambda=0.2) + coul_lambda=0.5) sigmas2 = list() epsilons2 = list() @@ -842,7 +902,7 @@ def test_coul1_lambda(self): 'Unexpected ratio between SASA radii' for charge1, charge2 in zip(charges1, charges2): - assert charge2 - charge1 < 1e-5, \ + assert (charge2 / charge1) - 0.8 < 1e-5, \ 'Unexpected ratio between charges' for bond_sc1, bond_sc2 in zip(bond_spring_constants1, @@ -866,7 +926,7 @@ def test_coul1_lambda(self): 'Unexpected ratio between improper constants' top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - coul1_lambda=0.0) + coul_lambda=0.0) sigmas1 = list() epsilons1 = list() @@ -910,8 +970,8 @@ def test_coul1_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants1.append(improper.constant) - top = alchemizer.get_alchemical_topology(fep_lambda=1.0, - coul1_lambda=1.0) + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul_lambda=0.5) sigmas2 = list() epsilons2 = list() @@ -955,45 +1015,111 @@ def test_coul1_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants2.append(improper.constant) - for sigma1, sigma2 in zip(sigmas1, sigmas2): - assert sigma2 - sigma1 < 1e-5, \ + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul_lambda=1.0) + + sigmas3 = list() + epsilons3 = list() + SASA_radii3 = list() + charges3 = list() + bond_spring_constants3 = list() + angle_spring_constants3 = list() + proper_constants3 = list() + improper_constants3 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas3.append(atom.sigma) + epsilons3.append(atom.epsilon) + SASA_radii3.append(atom.SASA_radius) + charges3.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants3.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants3.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants3.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants3.append(improper.constant) + + for sigma1, sigma2, sigma3 in zip(sigmas1, sigmas2, sigmas3): + assert sigma1 - sigma2 < 1e-5, \ + 'Unexpected ratio between sigmas' + assert sigma1 - sigma3 < 1e-5, \ 'Unexpected ratio between sigmas' - for epsilon1, epsilon2 in zip(epsilons1, epsilons2): - assert epsilon2 - epsilon1 < 1e-5, \ + for epsilon1, epsilon2, epsilon3 in zip(epsilons1, epsilons2, epsilons3): + assert epsilon1 - epsilon2 < 1e-5, \ + 'Unexpected ratio between epsilons' + assert epsilon1 - epsilon3 < 1e-5, \ 'Unexpected ratio between epsilons' - for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): - assert SASA_radius2 - SASA_radius1 < 1e-5, \ + for radius1, radius2, radius3 in zip(SASA_radii1, SASA_radii2, + SASA_radii3): + assert radius1 - radius2 < 1e-5, \ + 'Unexpected ratio between SASA radii' + assert radius1 - radius3 < 1e-5, \ 'Unexpected ratio between SASA radii' - for charge1, charge2 in zip(charges1, charges2): - assert charge2 - charge1 < 1e-5, \ + for charge1, charge2, charge3 in zip(charges1, charges2, charges3): + assert charge1 / charge2 - charge2 / charge3 < 1e-5, \ 'Unexpected ratio between charges' + assert abs(charge1 - charge2) > 1e-5, \ + 'Unexpected invariant charges' - for bond_sc1, bond_sc2 in zip(bond_spring_constants1, - bond_spring_constants2): - assert bond_sc2 - bond_sc1 < 1e-5, \ + for bond_sc1, bond_sc2, bond_sc3 in zip(bond_spring_constants1, + bond_spring_constants2, + bond_spring_constants3): + assert bond_sc1 - bond_sc2 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + assert bond_sc1 - bond_sc3 < 1e-5, \ 'Unexpected ratio between bond spring constants' - for angle_sc1, angle_sc2 in zip(angle_spring_constants1, - angle_spring_constants2): - assert angle_sc2 - angle_sc1 < 1e-5, \ + for angle_sc1, angle_sc2, angle_sc3 in zip(angle_spring_constants1, + angle_spring_constants2, + angle_spring_constants3): + assert angle_sc1 - angle_sc2 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + assert angle_sc1 - angle_sc3 < 1e-5, \ 'Unexpected ratio between angle spring constants' - for proper_c1, proper_c2 in zip(proper_constants1, - proper_constants2): - assert proper_c2 - proper_c1 < 1e-5, \ + for proper_c1, proper_c2, proper_c3 in zip(proper_constants1, + proper_constants2, + proper_constants3): + assert proper_c1 - proper_c2 < 1e-5, \ + 'Unexpected ratio between proper constants' + assert proper_c1 - proper_c3 < 1e-5, \ 'Unexpected ratio between proper constants' - for improper_c1, improper_c2 in zip(improper_constants1, - improper_constants2): - assert improper_c2 - improper_c1 < 1e-5, \ + for improper_c1, improper_c2, improper_c3 in zip(improper_constants1, + improper_constants2, + improper_constants3): + assert improper_c1 - improper_c2 < 1e-5, \ + 'Unexpected ratio between improper constants' + assert improper_c1 - improper_c3 < 1e-5, \ 'Unexpected ratio between improper constants' - def test_coul2_lambda(self): + def test_coul1_lambda(self): """ - It validates the effects of coul2 lambda on atom parameters. + It validates the effects of coul1 lambda on atom parameters. """ from peleffy.topology import Alchemizer from peleffy.template.impact import (WritableAtom, WritableBond, @@ -1007,7 +1133,7 @@ def test_coul2_lambda(self): alchemizer = Alchemizer(top1, top2) top = alchemizer.get_alchemical_topology(fep_lambda=0, - coul2_lambda=0) + coul1_lambda=0) sigmas1 = list() epsilons1 = list() @@ -1042,7 +1168,7 @@ def test_coul2_lambda(self): improper_constants1.append(improper.spring_constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - coul2_lambda=0.2) + coul1_lambda=0.2) sigmas2 = list() epsilons2 = list() @@ -1089,7 +1215,7 @@ def test_coul2_lambda(self): 'Unexpected ratio between SASA radii' for charge1, charge2 in zip(charges1, charges2): - assert charge2 - charge1 < 1e-5, \ + assert (charge2 / charge1) - (1 - 0.2) < 1e-5, \ 'Unexpected ratio between charges' for bond_sc1, bond_sc2 in zip(bond_spring_constants1, @@ -1113,7 +1239,7 @@ def test_coul2_lambda(self): 'Unexpected ratio between improper constants' top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - coul2_lambda=1.0) + coul1_lambda=0.0) sigmas1 = list() epsilons1 = list() @@ -1148,7 +1274,7 @@ def test_coul2_lambda(self): improper_constants1.append(improper.spring_constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - coul2_lambda=0.2) + coul1_lambda=0.2) sigmas2 = list() epsilons2 = list() @@ -1195,7 +1321,7 @@ def test_coul2_lambda(self): 'Unexpected ratio between SASA radii' for charge1, charge2 in zip(charges1, charges2): - assert (charge2 / charge1) - 0.8 < 1e-5, \ + assert charge2 - charge1 < 1e-5, \ 'Unexpected ratio between charges' for bond_sc1, bond_sc2 in zip(bond_spring_constants1, @@ -1219,7 +1345,7 @@ def test_coul2_lambda(self): 'Unexpected ratio between improper constants' top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - coul2_lambda=0.0) + coul1_lambda=0.0) sigmas1 = list() epsilons1 = list() @@ -1263,8 +1389,8 @@ def test_coul2_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants1.append(improper.constant) - top = alchemizer.get_alchemical_topology(fep_lambda=1.0, - coul2_lambda=1.0) + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul1_lambda=0.5) sigmas2 = list() epsilons2 = list() @@ -1308,45 +1434,111 @@ def test_coul2_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants2.append(improper.constant) - for sigma1, sigma2 in zip(sigmas1, sigmas2): - assert sigma2 - sigma1 < 1e-5, \ + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul1_lambda=1.0) + + sigmas3 = list() + epsilons3 = list() + SASA_radii3 = list() + charges3 = list() + bond_spring_constants3 = list() + angle_spring_constants3 = list() + proper_constants3 = list() + improper_constants3 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas3.append(atom.sigma) + epsilons3.append(atom.epsilon) + SASA_radii3.append(atom.SASA_radius) + charges3.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants3.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants3.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants3.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants3.append(improper.constant) + + for sigma1, sigma2, sigma3 in zip(sigmas1, sigmas2, sigmas3): + assert sigma1 - sigma2 < 1e-5, \ + 'Unexpected ratio between sigmas' + assert sigma1 - sigma3 < 1e-5, \ 'Unexpected ratio between sigmas' - for epsilon1, epsilon2 in zip(epsilons1, epsilons2): - assert epsilon2 - epsilon1 < 1e-5, \ + for epsilon1, epsilon2, epsilon3 in zip(epsilons1, epsilons2, epsilons3): + assert epsilon1 - epsilon2 < 1e-5, \ + 'Unexpected ratio between epsilons' + assert epsilon1 - epsilon3 < 1e-5, \ 'Unexpected ratio between epsilons' - for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): - assert SASA_radius2 - SASA_radius1 < 1e-5, \ + for radius1, radius2, radius3 in zip(SASA_radii1, SASA_radii2, + SASA_radii3): + assert radius1 - radius2 < 1e-5, \ + 'Unexpected ratio between SASA radii' + assert radius1 - radius3 < 1e-5, \ 'Unexpected ratio between SASA radii' - for charge1, charge2 in zip(charges1, charges2): - assert charge2 - charge1 < 1e-5, \ + for charge1, charge2, charge3 in zip(charges1, charges2, charges3): + assert charge1 - charge2 < 1e-5, \ + 'Unexpected ratio between charges' + assert charge1 - charge3 < 1e-5, \ 'Unexpected ratio between charges' - for bond_sc1, bond_sc2 in zip(bond_spring_constants1, - bond_spring_constants2): - assert bond_sc2 - bond_sc1 < 1e-5, \ + for bond_sc1, bond_sc2, bond_sc3 in zip(bond_spring_constants1, + bond_spring_constants2, + bond_spring_constants3): + assert bond_sc1 - bond_sc2 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + assert bond_sc1 - bond_sc3 < 1e-5, \ 'Unexpected ratio between bond spring constants' - for angle_sc1, angle_sc2 in zip(angle_spring_constants1, - angle_spring_constants2): - assert angle_sc2 - angle_sc1 < 1e-5, \ + for angle_sc1, angle_sc2, angle_sc3 in zip(angle_spring_constants1, + angle_spring_constants2, + angle_spring_constants3): + assert angle_sc1 - angle_sc2 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + assert angle_sc1 - angle_sc3 < 1e-5, \ 'Unexpected ratio between angle spring constants' - for proper_c1, proper_c2 in zip(proper_constants1, - proper_constants2): - assert proper_c2 - proper_c1 < 1e-5, \ + for proper_c1, proper_c2, proper_c3 in zip(proper_constants1, + proper_constants2, + proper_constants3): + assert proper_c1 - proper_c2 < 1e-5, \ + 'Unexpected ratio between proper constants' + assert proper_c1 - proper_c3 < 1e-5, \ 'Unexpected ratio between proper constants' - for improper_c1, improper_c2 in zip(improper_constants1, - improper_constants2): - assert improper_c2 - improper_c1 < 1e-5, \ + for improper_c1, improper_c2, improper_c3 in zip(improper_constants1, + improper_constants2, + improper_constants3): + assert improper_c1 - improper_c2 < 1e-5, \ + 'Unexpected ratio between improper constants' + assert improper_c1 - improper_c3 < 1e-5, \ 'Unexpected ratio between improper constants' - def test_vdw_lambda(self): + def test_coul2_lambda(self): """ - It validates the effects of vdw lambda on atom parameters. + It validates the effects of coul2 lambda on atom parameters. """ from peleffy.topology import Alchemizer from peleffy.template.impact import (WritableAtom, WritableBond, @@ -1360,7 +1552,7 @@ def test_vdw_lambda(self): alchemizer = Alchemizer(top1, top2) top = alchemizer.get_alchemical_topology(fep_lambda=0, - vdw_lambda=0) + coul2_lambda=0) sigmas1 = list() epsilons1 = list() @@ -1395,7 +1587,7 @@ def test_vdw_lambda(self): improper_constants1.append(improper.spring_constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - vdw_lambda=0.2) + coul2_lambda=0.2) sigmas2 = list() epsilons2 = list() @@ -1430,15 +1622,15 @@ def test_vdw_lambda(self): improper_constants2.append(improper.spring_constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): - assert (sigma2 / sigma1) - (1 - 0.2) < 1e-5, \ + assert sigma2 - sigma1 < 1e-5, \ 'Unexpected ratio between sigmas' for epsilon1, epsilon2 in zip(epsilons1, epsilons2): - assert (epsilon2 / epsilon1) - (1 - 0.2) < 1e-5, \ + assert epsilon2 - epsilon1 < 1e-5, \ 'Unexpected ratio between epsilons' for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): - assert (SASA_radius2 / SASA_radius1) - (1 - 0.2) < 1e-5, \ + assert SASA_radius2 - SASA_radius1 < 1e-5, \ 'Unexpected ratio between SASA radii' for charge1, charge2 in zip(charges1, charges2): @@ -1466,7 +1658,7 @@ def test_vdw_lambda(self): 'Unexpected ratio between improper constants' top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - vdw_lambda=1.0) + coul2_lambda=1.0) sigmas1 = list() epsilons1 = list() @@ -1501,7 +1693,7 @@ def test_vdw_lambda(self): improper_constants1.append(improper.spring_constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - vdw_lambda=0.2) + coul2_lambda=0.2) sigmas2 = list() epsilons2 = list() @@ -1536,19 +1728,19 @@ def test_vdw_lambda(self): improper_constants2.append(improper.spring_constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): - assert (sigma2 / sigma1) - 0.2 < 1e-5, \ + assert sigma2 - sigma1 < 1e-5, \ 'Unexpected ratio between sigmas' for epsilon1, epsilon2 in zip(epsilons1, epsilons2): - assert (epsilon2 / epsilon1) - 0.2 < 1e-5, \ + assert epsilon2 - epsilon1 < 1e-5, \ 'Unexpected ratio between epsilons' for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): - assert (SASA_radius2 / SASA_radius1) - 0.2 < 1e-5, \ + assert SASA_radius2 - SASA_radius1 < 1e-5, \ 'Unexpected ratio between SASA radii' for charge1, charge2 in zip(charges1, charges2): - assert charge2 - charge1 < 1e-5, \ + assert (charge2 / charge1) - 0.8 < 1e-5, \ 'Unexpected ratio between charges' for bond_sc1, bond_sc2 in zip(bond_spring_constants1, @@ -1572,7 +1764,7 @@ def test_vdw_lambda(self): 'Unexpected ratio between improper constants' top = alchemizer.get_alchemical_topology(fep_lambda=0.0, - vdw_lambda=0.0) + coul2_lambda=0.0) sigmas1 = list() epsilons1 = list() @@ -1616,8 +1808,8 @@ def test_vdw_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants1.append(improper.constant) - top = alchemizer.get_alchemical_topology(fep_lambda=1.0, - vdw_lambda=1.0) + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul2_lambda=0.5) sigmas2 = list() epsilons2 = list() @@ -1661,40 +1853,523 @@ def test_vdw_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants2.append(improper.constant) - for sigma1, sigma2 in zip(sigmas1, sigmas2): - assert sigma2 - sigma1 < 1e-5, \ - 'Unexpected ratio between sigmas' + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + coul2_lambda=1.0) - for epsilon1, epsilon2 in zip(epsilons1, epsilons2): - assert epsilon2 - epsilon1 < 1e-5, \ - 'Unexpected ratio between epsilons' + sigmas3 = list() + epsilons3 = list() + SASA_radii3 = list() + charges3 = list() + bond_spring_constants3 = list() + angle_spring_constants3 = list() + proper_constants3 = list() + improper_constants3 = list() - for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): - assert SASA_radius2 - SASA_radius1 < 1e-5, \ - 'Unexpected ratio between SASA radii' + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas3.append(atom.sigma) + epsilons3.append(atom.epsilon) + SASA_radii3.append(atom.SASA_radius) + charges3.append(atom.charge) - for charge1, charge2 in zip(charges1, charges2): - assert charge2 - charge1 < 1e-5, \ - 'Unexpected ratio between charges' + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants3.append(bond.spring_constant) - for bond_sc1, bond_sc2 in zip(bond_spring_constants1, - bond_spring_constants2): - assert bond_sc2 - bond_sc1 < 1e-5, \ - 'Unexpected ratio between bond spring constants' + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants3.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants3.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants3.append(improper.constant) + + for sigma1, sigma2, sigma3 in zip(sigmas1, sigmas2, sigmas3): + assert sigma1 - sigma2 < 1e-5, \ + 'Unexpected ratio between sigmas' + assert sigma1 - sigma3 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2, epsilon3 in zip(epsilons1, epsilons2, epsilons3): + assert epsilon1 - epsilon2 < 1e-5, \ + 'Unexpected ratio between epsilons' + assert epsilon1 - epsilon3 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for radius1, radius2, radius3 in zip(SASA_radii1, SASA_radii2, + SASA_radii3): + assert radius1 - radius2 < 1e-5, \ + 'Unexpected ratio between SASA radii' + assert radius1 - radius3 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2, charge3 in zip(charges1, charges2, charges3): + assert charge1 - charge2 < 1e-5, \ + 'Unexpected ratio between charges' + assert charge1 - charge3 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2, bond_sc3 in zip(bond_spring_constants1, + bond_spring_constants2, + bond_spring_constants3): + assert bond_sc1 - bond_sc2 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + assert bond_sc1 - bond_sc3 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2, angle_sc3 in zip(angle_spring_constants1, + angle_spring_constants2, + angle_spring_constants3): + assert angle_sc1 - angle_sc2 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + assert angle_sc1 - angle_sc3 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2, proper_c3 in zip(proper_constants1, + proper_constants2, + proper_constants3): + assert proper_c1 - proper_c2 < 1e-5, \ + 'Unexpected ratio between proper constants' + assert proper_c1 - proper_c3 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2, improper_c3 in zip(improper_constants1, + improper_constants2, + improper_constants3): + assert improper_c1 - improper_c2 < 1e-5, \ + 'Unexpected ratio between improper constants' + assert improper_c1 - improper_c3 < 1e-5, \ + 'Unexpected ratio between improper constants' + + def test_vdw_lambda(self): + """ + It validates the effects of vdw lambda on atom parameters. + """ + from peleffy.topology import Alchemizer + from peleffy.template.impact import (WritableAtom, WritableBond, + WritableAngle, WritableProper, + WritableImproper) + + mol1, mol2, top1, top2 = \ + generate_molecules_and_topologies_from_smiles('C=C', + 'C(Cl)(Cl)(Cl)') + + alchemizer = Alchemizer(top1, top2) + + top = alchemizer.get_alchemical_topology(fep_lambda=0, + vdw_lambda=0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._exclusive_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._exclusive_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._exclusive_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._exclusive_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._exclusive_propers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert (sigma2 / sigma1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert (epsilon2 / epsilon1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert (SASA_radius2 / SASA_radius1) - (1 - 0.2) < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=1.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.spring_constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=0.2) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in alchemizer._non_native_atoms: + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in alchemizer._non_native_bonds: + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in alchemizer._non_native_angles: + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in alchemizer._non_native_propers: + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.spring_constant) + + for improper_idx in alchemizer._non_native_impropers: + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.spring_constant) + + for sigma1, sigma2 in zip(sigmas1, sigmas2): + assert (sigma2 / sigma1) - 0.2 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2 in zip(epsilons1, epsilons2): + assert (epsilon2 / epsilon1) - 0.2 < 1e-5, \ + 'Unexpected ratio between epsilons' + + for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): + assert (SASA_radius2 / SASA_radius1) - 0.2 < 1e-5, \ + 'Unexpected ratio between SASA radii' + + for charge1, charge2 in zip(charges1, charges2): + assert charge2 - charge1 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2 in zip(bond_spring_constants1, + bond_spring_constants2): + assert bond_sc2 - bond_sc1 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2 in zip(angle_spring_constants1, + angle_spring_constants2): + assert angle_sc2 - angle_sc1 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + + for proper_c1, proper_c2 in zip(proper_constants1, + proper_constants2): + assert proper_c2 - proper_c1 < 1e-5, \ + 'Unexpected ratio between proper constants' + + for improper_c1, improper_c2 in zip(improper_constants1, + improper_constants2): + assert improper_c2 - improper_c1 < 1e-5, \ + 'Unexpected ratio between improper constants' + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=0.0) + + sigmas1 = list() + epsilons1 = list() + SASA_radii1 = list() + charges1 = list() + bond_spring_constants1 = list() + angle_spring_constants1 = list() + proper_constants1 = list() + improper_constants1 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas1.append(atom.sigma) + epsilons1.append(atom.epsilon) + SASA_radii1.append(atom.SASA_radius) + charges1.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants1.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants1.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants1.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants1.append(improper.constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=0.5) + + sigmas2 = list() + epsilons2 = list() + SASA_radii2 = list() + charges2 = list() + bond_spring_constants2 = list() + angle_spring_constants2 = list() + proper_constants2 = list() + improper_constants2 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas2.append(atom.sigma) + epsilons2.append(atom.epsilon) + SASA_radii2.append(atom.SASA_radius) + charges2.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants2.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants2.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants2.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants2.append(improper.constant) + + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + vdw_lambda=1.0) + + sigmas3 = list() + epsilons3 = list() + SASA_radii3 = list() + charges3 = list() + bond_spring_constants3 = list() + angle_spring_constants3 = list() + proper_constants3 = list() + improper_constants3 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas3.append(atom.sigma) + epsilons3.append(atom.epsilon) + SASA_radii3.append(atom.SASA_radius) + charges3.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants3.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants3.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants3.append(proper.constant) - for angle_sc1, angle_sc2 in zip(angle_spring_constants1, - angle_spring_constants2): - assert angle_sc2 - angle_sc1 < 1e-5, \ + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants3.append(improper.constant) + + for sigma1, sigma2, sigma3 in zip(sigmas1, sigmas2, sigmas3): + assert sigma1 / sigma2 - sigma2 / sigma3 < 1e-5, \ + 'Unexpected ratio between sigmas' + + for epsilon1, epsilon2, epsilon3 in zip(epsilons1, epsilons2, epsilons3): + assert epsilon1 / epsilon2 - epsilon2 / epsilon3 < 1e-5, \ + 'Unexpected ratio between epsilons' + assert abs(epsilon1 - epsilon2) > 1e-5, \ + 'Unexpected invariant epsilons' + + for radius1, radius2, radius3 in zip(SASA_radii1, SASA_radii2, + SASA_radii3): + assert radius1 / radius2 - radius2 / radius3 < 1e-5, \ + 'Unexpected ratio between SASA radii' + assert abs(radius1 - radius2) > 1e-5, \ + 'Unexpected invariant SASA radii' + + for charge1, charge2, charge3 in zip(charges1, charges2, charges3): + assert charge1 - charge2 < 1e-5, \ + 'Unexpected ratio between charges' + assert charge1 - charge3 < 1e-5, \ + 'Unexpected ratio between charges' + + for bond_sc1, bond_sc2, bond_sc3 in zip(bond_spring_constants1, + bond_spring_constants2, + bond_spring_constants3): + assert bond_sc1 - bond_sc2 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + assert bond_sc1 - bond_sc3 < 1e-5, \ + 'Unexpected ratio between bond spring constants' + + for angle_sc1, angle_sc2, angle_sc3 in zip(angle_spring_constants1, + angle_spring_constants2, + angle_spring_constants3): + assert angle_sc1 - angle_sc2 < 1e-5, \ + 'Unexpected ratio between angle spring constants' + assert angle_sc1 - angle_sc3 < 1e-5, \ 'Unexpected ratio between angle spring constants' - for proper_c1, proper_c2 in zip(proper_constants1, - proper_constants2): - assert proper_c2 - proper_c1 < 1e-5, \ + for proper_c1, proper_c2, proper_c3 in zip(proper_constants1, + proper_constants2, + proper_constants3): + assert proper_c1 - proper_c2 < 1e-5, \ + 'Unexpected ratio between proper constants' + assert proper_c1 - proper_c3 < 1e-5, \ 'Unexpected ratio between proper constants' - for improper_c1, improper_c2 in zip(improper_constants1, - improper_constants2): - assert improper_c2 - improper_c1 < 1e-5, \ + for improper_c1, improper_c2, improper_c3 in zip(improper_constants1, + improper_constants2, + improper_constants3): + assert improper_c1 - improper_c2 < 1e-5, \ + 'Unexpected ratio between improper constants' + assert improper_c1 - improper_c3 < 1e-5, \ 'Unexpected ratio between improper constants' def test_bonded_lambda(self): @@ -1969,8 +2644,8 @@ def test_bonded_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants1.append(improper.constant) - top = alchemizer.get_alchemical_topology(fep_lambda=1.0, - bonded_lambda=1.0) + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + bonded_lambda=0.5) sigmas2 = list() epsilons2 = list() @@ -2014,45 +2689,107 @@ def test_bonded_lambda(self): improper = WritableImproper(top.impropers[improper_idx]) improper_constants2.append(improper.constant) - for sigma1, sigma2 in zip(sigmas1, sigmas2): - assert sigma2 - sigma1 < 1e-5, \ + top = alchemizer.get_alchemical_topology(fep_lambda=0.0, + bonded_lambda=1.0) + + sigmas3 = list() + epsilons3 = list() + SASA_radii3 = list() + charges3 = list() + bond_spring_constants3 = list() + angle_spring_constants3 = list() + proper_constants3 = list() + improper_constants3 = list() + + for atom_idx in range(0, len(top.atoms)): + if (atom_idx not in alchemizer._exclusive_atoms and + atom_idx not in alchemizer._non_native_atoms): + atom = WritableAtom(top.atoms[atom_idx]) + sigmas3.append(atom.sigma) + epsilons3.append(atom.epsilon) + SASA_radii3.append(atom.SASA_radius) + charges3.append(atom.charge) + + for bond_idx in range(0, len(top.bonds)): + if (bond_idx not in alchemizer._exclusive_bonds and + bond_idx not in alchemizer._non_native_bonds): + bond = WritableBond(top.bonds[bond_idx]) + bond_spring_constants3.append(bond.spring_constant) + + for angle_idx in range(0, len(top.angles)): + if (angle_idx not in alchemizer._exclusive_angles and + angle_idx not in alchemizer._non_native_angles): + angle = WritableAngle(top.angles[angle_idx]) + angle_spring_constants3.append(angle.spring_constant) + + for proper_idx in range(0, len(top.propers)): + if (proper_idx not in alchemizer._exclusive_propers and + proper_idx not in alchemizer._non_native_propers): + proper = WritableProper(top.propers[proper_idx]) + proper_constants3.append(proper.constant) + + for improper_idx in range(0, len(top.impropers)): + if (improper_idx not in alchemizer._exclusive_impropers and + improper_idx not in alchemizer._non_native_impropers): + improper = WritableImproper(top.impropers[improper_idx]) + improper_constants3.append(improper.constant) + + for sigma1, sigma2, sigma3 in zip(sigmas1, sigmas2, sigmas3): + assert sigma1 - sigma2 < 1e-5, \ + 'Unexpected ratio between sigmas' + assert sigma1 - sigma3 < 1e-5, \ 'Unexpected ratio between sigmas' - for epsilon1, epsilon2 in zip(epsilons1, epsilons2): - assert epsilon2 - epsilon1 < 1e-5, \ + for epsilon1, epsilon2, epsilon3 in zip(epsilons1, epsilons2, epsilons3): + assert epsilon1 - epsilon2 < 1e-5, \ + 'Unexpected ratio between epsilons' + assert epsilon1 - epsilon3 < 1e-5, \ 'Unexpected ratio between epsilons' - for SASA_radius1, SASA_radius2 in zip(SASA_radii1, SASA_radii2): - assert SASA_radius2 - SASA_radius1 < 1e-5, \ + for radius1, radius2, radius3 in zip(SASA_radii1, SASA_radii2, + SASA_radii3): + assert radius1 - radius2 < 1e-5, \ + 'Unexpected ratio between SASA radii' + assert radius1 - radius3 < 1e-5, \ 'Unexpected ratio between SASA radii' - for charge1, charge2 in zip(charges1, charges2): - assert charge2 - charge1 < 1e-5, \ + for charge1, charge2, charge3 in zip(charges1, charges2, charges3): + assert charge1 - charge2 < 1e-5, \ + 'Unexpected ratio between charges' + assert charge1 - charge3 < 1e-5, \ 'Unexpected ratio between charges' - for bond_sc1, bond_sc2 in zip(bond_spring_constants1, - bond_spring_constants2): - assert bond_sc2 - bond_sc1 < 1e-5, \ + for bond_sc1, bond_sc2, bond_sc3 in zip(bond_spring_constants1, + bond_spring_constants2, + bond_spring_constants3): + assert bond_sc1 / bond_sc2 - bond_sc2 / bond_sc3 < 1e-5, \ 'Unexpected ratio between bond spring constants' + assert abs(bond_sc1 - bond_sc2) > 1e-5, \ + 'Unexpected invariant bond spring constants' - for angle_sc1, angle_sc2 in zip(angle_spring_constants1, - angle_spring_constants2): - assert angle_sc2 - angle_sc1 < 1e-5, \ + for angle_sc1, angle_sc2, angle_sc3 in zip(angle_spring_constants1, + angle_spring_constants2, + angle_spring_constants3): + assert angle_sc1 / angle_sc2 - angle_sc2 / angle_sc3 < 1e-5, \ 'Unexpected ratio between angle spring constants' + assert abs(angle_sc1 - angle_sc2) > 1e-5, \ + 'Unexpected invariant angle spring constants' - for proper_c1, proper_c2 in zip(proper_constants1, - proper_constants2): - assert proper_c2 - proper_c1 < 1e-5, \ + for proper_c1, proper_c2, proper_c3 in zip(proper_constants1, + proper_constants2, + proper_constants3): + assert proper_c1 / proper_c2 - proper_c2 / proper_c3 < 1e-5, \ 'Unexpected ratio between proper constants' - for improper_c1, improper_c2 in zip(improper_constants1, - improper_constants2): - assert improper_c2 - improper_c1 < 1e-5, \ + for improper_c1, improper_c2, improper_c3 in zip(improper_constants1, + improper_constants2, + improper_constants3): + assert improper_c1 / improper_c2 - improper_c2 / improper_c3 < 1e-5, \ 'Unexpected ratio between improper constants' @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, " + - "fep_lambda, coul1_lambda, coul2_lambda, " + - "vdw_lambda, bonded_lambda, " + + "fep_lambda, coul_lambda, coul1_lambda," + "coul2_lambda, vdw_lambda, bonded_lambda, " + "golden_sigmas, golden_epsilons, " + "golden_born_radii, golden_SASA_radii, " + "golden_nonpolar_gammas, " + @@ -2066,12 +2803,14 @@ def test_bonded_lambda(self): None, None, None, + None, [3.480646886945065, 3.480646886945065, 2.5725815350632795, 2.5725815350632795, 2.5725815350632795, 2.5725815350632795, 0.0], [0.0868793154488, 0.0868793154488, 0.01561134320353, 0.01561134320353, - 0.01561134320353, 0.01561134320353, 0.0], + 0.01561134320353, + 0.01561134320353, 0.0], [0, 0, 0, 0, 0, 0, 0], [1.7403234434725325, 1.7403234434725325, 1.2862907675316397, 1.2862907675316397, @@ -2090,23 +2829,25 @@ def test_bonded_lambda(self): None, None, None, - [3.480646886945065, 3.480646886945065, - 2.5725815350632795, 2.5725815350632795, + None, + [3.460423861881376, 3.446023070848177, + 2.7195707893427485, 2.481063939423657, 2.0580652280506238, 2.0580652280506238, 0.6615055612921249], - [0.0868793154488, 0.0868793154488, - 0.01561134320353, 0.01561134320353, - 0.012489074562824, - 0.012489074562824, 0.05312002093054], + [0.09127157454406, 0.12262347328957998, + 0.065609095493364, 0.015629074562824, + 0.012489074562824, 0.012489074562824, + 0.05312002093054], [0, 0, 0, 0, 0, 0, 0], - [1.7403234434725325, 1.7403234434725325, - 1.2862907675316397, 1.2862907675316397, + [1.730211930940688, 1.7230115354240885, + 1.3597853946713743, 1.2405319697118284, 1.0290326140253119, 1.0290326140253119, 0.33075278064606245], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], - [-0.106311, -0.106311, 0.053156, 0.053156, - 0.0425248, 0.0425248, -0.017483000000000002] + [-0.049028200000000015, -0.1025318, + 0.025041800000000003, 0.0589528, 0.0425248, + 0.0425248, -0.017483000000000002] ), (None, None, @@ -2117,24 +2858,26 @@ def test_bonded_lambda(self): None, None, None, - [3.480646886945065, 3.480646886945065, - 2.5725815350632795, 2.5725815350632795, + None, + [3.3997547866903095, 3.3421516225575125, + 3.1605385521811553, 2.2065111525047882, 0.5145163070126558, 0.5145163070126558, 2.6460222451684996], - [0.0868793154488, 0.0868793154488, - 0.01561134320353, 0.01561134320353, + [0.10444835182984, 0.22985594681192, + 0.215602352362866, 0.015682268640705998, 0.003122268640705999, 0.003122268640705999, 0.21248008372216], [0, 0, 0, 0, 0, 0, 0], - [1.7403234434725325, 1.7403234434725325, - 1.2862907675316397, 1.2862907675316397, + [1.6998773933451548, 1.6710758112787563, + 1.5802692760905777, 1.1032555762523941, 0.2572581535063279, 0.2572581535063279, 1.3230111225842498], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], - [-0.106311, -0.106311, 0.053156, 0.053156, - 0.010631199999999999, 0.010631199999999999, - -0.06993200000000001] + [0.12282020000000003, -0.0911942, + -0.05930080000000001, 0.0763432, + 0.010631199999999999, + 0.010631199999999999, -0.06993200000000001] ), (None, None, @@ -2145,20 +2888,20 @@ def test_bonded_lambda(self): None, None, None, - [3.480646886945065, 3.480646886945065, - 2.5725815350632795, 2.5725815350632795, - 0.0, 0.0, - 3.3075278064606244], - [0.0868793154488, 0.0868793154488, - 0.01561134320353, 0.01561134320353, 0.0, 0.0, + None, + [3.3795317616266205, 3.3075278064606244, + 3.3075278064606244, 2.1149935568651657, 0.0, + 0.0, 3.3075278064606244], + [0.1088406109251, 0.2656001046527, + 0.2656001046527, 0.0157, 0.0, 0.0, 0.2656001046527], [0, 0, 0, 0, 0, 0, 0], - [1.7403234434725325, 1.7403234434725325, - 1.2862907675316397, 1.2862907675316397, 0.0, + [1.6897658808133103, 1.6537639032303122, + 1.6537639032303122, 1.0574967784325828, 0.0, 0.0, 1.6537639032303122], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], - [-0.106311, -0.106311, 0.053156, 0.053156, + [0.180103, -0.087415, -0.087415, 0.08214, 0.0, 0.0, -0.087415] ), (None, @@ -2166,6 +2909,7 @@ def test_bonded_lambda(self): 'C=C', 'C(Cl)(Cl)(Cl)', 0.0, + None, 0.0, 0.0, None, @@ -2191,6 +2935,7 @@ def test_bonded_lambda(self): 'C=C', 'C(Cl)(Cl)(Cl)', 0.0, + None, 1.0, 0.0, None, @@ -2216,23 +2961,24 @@ def test_bonded_lambda(self): 'C=C', 'C(Cl)(Cl)(Cl)', 1.0, + None, 1.0, 0.0, None, None, - [3.480646886945065, 3.480646886945065, - 2.5725815350632795, 2.5725815350632795, 0.0, 0.0, - 3.3075278064606244], - [0.0868793154488, 0.0868793154488, - 0.01561134320353, 0.01561134320353, 0.0, 0.0, + [3.3795317616266205, 3.3075278064606244, + 3.3075278064606244, 2.1149935568651657, 0.0, + 0.0, 3.3075278064606244], + [0.1088406109251, 0.2656001046527, + 0.2656001046527, 0.0157, 0.0, 0.0, 0.2656001046527], [0, 0, 0, 0, 0, 0, 0], - [1.7403234434725325, 1.7403234434725325, - 1.2862907675316397, 1.2862907675316397, 0.0, + [1.6897658808133103, 1.6537639032303122, + 1.6537639032303122, 1.0574967784325828, 0.0, 0.0, 1.6537639032303122], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], - [-0.106311, -0.106311, 0.053156, 0.053156, + [0.180103, -0.087415, -0.087415, 0.08214, 0.0, 0.0, -0.0] ), (None, @@ -2240,31 +2986,81 @@ def test_bonded_lambda(self): 'C=C', 'C(Cl)(Cl)(Cl)', 1.0, + None, 1.0, 1.0, None, None, - [3.480646886945065, 3.480646886945065, - 2.5725815350632795, 2.5725815350632795, - 0.0, 0.0, - 3.3075278064606244], - [0.0868793154488, 0.0868793154488, - 0.01561134320353, 0.01561134320353, 0.0, 0.0, + [3.3795317616266205, 3.3075278064606244, + 3.3075278064606244, 2.1149935568651657, 0.0, + 0.0, 3.3075278064606244], + [0.1088406109251, 0.2656001046527, + 0.2656001046527, 0.0157, 0.0, 0.0, 0.2656001046527], [0, 0, 0, 0, 0, 0, 0], - [1.7403234434725325, 1.7403234434725325, - 1.2862907675316397, 1.2862907675316397, 0.0, + [1.6897658808133103, 1.6537639032303122, + 1.6537639032303122, 1.0574967784325828, 0.0, 0.0, 1.6537639032303122], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], - [-0.106311, -0.106311, 0.053156, 0.053156, + [0.180103, -0.087415, -0.087415, 0.08214, 0.0, 0.0, -0.087415] ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 1.0, + 0.5, + 1.0, + 0.0, + None, + None, + [3.3795317616266205, 3.3075278064606244, + 3.3075278064606244, 2.1149935568651657, 0.0, + 0.0, 3.3075278064606244], + [0.1088406109251, 0.2656001046527, + 0.2656001046527, 0.0157, 0.0, 0.0, + 0.2656001046527], + [0, 0, 0, 0, 0, 0, 0], + [1.6897658808133103, 1.6537639032303122, + 1.6537639032303122, 1.0574967784325828, 0.0, + 0.0, 1.6537639032303122], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0.180103, -0.087415, -0.087415, 0.08214, + 0.0, 0.0, -0.0] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 1.0, + 1.0, + 0.0, + 1.0, + None, + None, + [3.3795317616266205, 3.3075278064606244, + 3.3075278064606244, 2.1149935568651657, 0.0, + 0.0, 3.3075278064606244], + [0.1088406109251, 0.2656001046527, + 0.2656001046527, 0.0157, 0.0, 0.0, + 0.2656001046527], + [0, 0, 0, 0, 0, 0, 0], + [1.6897658808133103, 1.6537639032303122, + 1.6537639032303122, 1.0574967784325828, 0.0, + 0.0, 1.6537639032303122], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [-0.106311, -0.106311, 0.053156, 0.053156, + 0.053156, 0.053156, -0.087415] + ) ]) def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, - fep_lambda, coul1_lambda, - coul2_lambda, vdw_lambda, - bonded_lambda, + fep_lambda, coul_lambda, + coul1_lambda, coul2_lambda, + vdw_lambda, bonded_lambda, golden_sigmas, golden_epsilons, golden_born_radii, @@ -2284,6 +3080,7 @@ def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, alchemizer = Alchemizer(top1, top2) top = alchemizer.get_alchemical_topology(fep_lambda=fep_lambda, + coul_lambda=coul_lambda, coul1_lambda=coul1_lambda, coul2_lambda=coul2_lambda, vdw_lambda=vdw_lambda, @@ -2315,8 +3112,9 @@ def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 'Unexpected non polar alphas' @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, " + - "fep_lambda, coul1_lambda, coul2_lambda, " + - "vdw_lambda, bonded_lambda, " + + "fep_lambda, coul_lambda, coul1_lambda, " + + "coul2_lambda, vdw_lambda, " + + "bonded_lambda, " + "golden_bond_spring_constants", [(None, None, @@ -2327,9 +3125,10 @@ def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, None, None, None, + None, [399.1592953295, 397.2545789619, 397.2545789619, 397.2545789619, - 397.2545789619, 172.40622182] + 397.2545789619, 0.0] ), (None, None, @@ -2340,9 +3139,10 @@ def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, None, None, None, - [399.1592953295, 397.2545789619, - 397.2545789619, 198.62728948095, - 198.62728948095, 172.40622182] + None, + [285.78275857475, 284.83040039095, + 383.650642924075, 198.62728948095, + 198.62728948095, 86.20311091] ), (None, None, @@ -2353,8 +3153,9 @@ def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, None, None, None, - [399.1592953295, 397.2545789619, - 397.2545789619, 0.0, 0.0, 172.40622182] + None, + [172.40622182, 172.40622182, + 370.04670688625, 0.0, 0.0, 172.40622182] ), (None, None, @@ -2364,10 +3165,11 @@ def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 1.0, 1.0, 1.0, + 1.0, 0.0, [399.1592953295, 397.2545789619, 397.2545789619, 397.2545789619, - 397.2545789619, 172.40622182] + 397.2545789619, 0.0] ), (None, None, @@ -2377,13 +3179,29 @@ def test_atoms_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 0.0, 0.0, 0.0, + 0.0, 1.0, - [399.1592953295, 397.2545789619, - 397.2545789619, 0.0, 0.0, 172.40622182] + [172.40622182, 172.40622182, 370.04670688625, + 0.0, 0.0, 172.40622182] + ), + (None, + None, + 'C=C', + 'C(Cl)(Cl)(Cl)', + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + [285.78275857475, 284.83040039095, + 383.650642924075, 198.62728948095, + 198.62728948095, 86.20311091] ) ]) - def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, - fep_lambda, coul1_lambda, + def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, + smiles2, fep_lambda, + coul_lambda, coul1_lambda, coul2_lambda, vdw_lambda, bonded_lambda, golden_bond_spring_constants): @@ -2399,6 +3217,7 @@ def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, alchemizer = Alchemizer(top1, top2) top = alchemizer.get_alchemical_topology(fep_lambda=fep_lambda, + coul_lambda=coul_lambda, coul1_lambda=coul1_lambda, coul2_lambda=coul2_lambda, vdw_lambda=vdw_lambda, @@ -2414,8 +3233,8 @@ def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 'Unexpected spring constants' @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, " + - "fep_lambda, coul1_lambda, coul2_lambda, " + - "vdw_lambda, bonded_lambda, " + + "fep_lambda, coul_lambda, coul1_lambda, " + + "coul2_lambda, vdw_lambda, bonded_lambda, " + "golden_angle_spring_constants", [(None, None, @@ -2426,6 +3245,7 @@ def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, None, None, None, + None, [34.066775159195, 34.066775159195, 34.066775159195, 34.066775159195, 22.69631544292, 22.69631544292, @@ -2440,9 +3260,10 @@ def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, None, None, None, + None, [17.0333875795975, 17.0333875795975, - 34.066775159195, 34.066775159195, - 22.69631544292, 11.34815772146, + 43.6360457123225, 43.6360457123225, + 37.950815854185, 11.34815772146, 26.602658132725, 26.602658132725, 26.602658132725] ), @@ -2455,8 +3276,9 @@ def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, None, None, None, - [0.0, 0.0, 34.066775159195, 34.066775159195, - 22.69631544292, 0.0, 53.20531626545, + None, + [0.0, 0.0, 53.20531626545, 53.20531626545, + 53.20531626545, 0.0, 53.20531626545, 53.20531626545, 53.20531626545] ), (None, @@ -2467,6 +3289,7 @@ def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 1.0, 1.0, 1.0, + 1.0, 0.0, [34.066775159195, 34.066775159195, 34.066775159195, 34.066775159195, @@ -2481,14 +3304,17 @@ def test_bonds_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 0.0, 0.0, 0.0, + 0.0, 1.0, - [0.0, 0.0, 34.066775159195, 34.066775159195, - 22.69631544292, 0.0, 53.20531626545, - 53.20531626545, 53.20531626545] + [0.0, 0.0, 53.20531626545, + 53.20531626545, 53.20531626545, + 0.0, 53.20531626545, 53.20531626545, + 53.20531626545] ) ]) - def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, - fep_lambda, coul1_lambda, + def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, + smiles2, fep_lambda, + coul_lambda, coul1_lambda, coul2_lambda, vdw_lambda, bonded_lambda, golden_angle_spring_constants): @@ -2519,8 +3345,8 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 'Unexpected spring constants' @pytest.mark.parametrize("pdb1, pdb2, smiles1, smiles2, " + - "fep_lambda, coul1_lambda, coul2_lambda, " + - "vdw_lambda, bonded_lambda, " + + "fep_lambda, coul_lambda, coul1_lambda, " + "coul2_lambda, vdw_lambda, bonded_lambda, " + "golden_proper_constants, " "golden_improper_constants", [(None, @@ -2532,6 +3358,7 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, None, None, None, + None, [0.06697375586735, 0.06697375586735, 0.06697375586735, 0.06697375586735, 0.06697375586735, 0.06697375586735, @@ -2555,12 +3382,50 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 0.02664938770063, 0.02664938770063, 0.1489710476446, 0.1489710476446, 0.02960027280666, 0.02960027280666, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, - -0.0], - [10.5, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1] + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, -0.0, -0.0], + [10.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + ), + (None, + None, + 'C[N+](C)(C)CC(=O)[O-]', + '[NH]=C(N)c1ccccc1', + 1.0, + None, + None, + None, + None, + None, + [1.256156174911, 1.256156174911, + 0.06697375586735, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, -0.0, 0.0, + 6.736762477654, 0.06697375586735, + 0.06697375586735, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.9974165607242, + 0.9974165607242, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 0.9974165607242, + 0.9974165607242, 6.736762477654, + 1.809047390003, 1.809047390003, + 3.661930099076,3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + 3.661930099076, 3.661930099076, + -0.04816277792592, -0.04816277792592], + [0.0, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1] ), (None, None, @@ -2571,12 +3436,13 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, None, None, None, - [0.06697375586735, 0.06697375586735, + None, + [1.2319154369245875, 1.2319154369245875, 0.06697375586735, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, -0.18516762066095, - 0.013324693850315, 0.06697375586735, + 0.013324693850315, 3.401868116760675, 0.06697375586735, 0.06697375586735, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, @@ -2611,48 +3477,13 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 1.830965049538, 1.830965049538, 1.830965049538, -0.02408138896296, -0.02408138896296], - [10.5, 1.1, 1.1, 0.55, 0.55, 0.55, 0.55] + [5.25, 0.55, 0.55, 0.55, 0.55, 0.55, 0.55] ), (None, None, 'C[N+](C)(C)CC(=O)[O-]', '[NH]=C(N)c1ccccc1', 1.0, - None, - None, - None, - None, - [0.06697375586735, 0.06697375586735, - 0.06697375586735, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, -0.0, 0.0, 0.06697375586735, - 0.06697375586735, 0.06697375586735, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.9974165607242, 0.9974165607242, - 3.661930099076, 3.661930099076, - 3.661930099076, 3.661930099076, - 0.9974165607242, 0.9974165607242, - 6.736762477654, 1.809047390003, - 1.809047390003, 3.661930099076, - 3.661930099076, 3.661930099076, - 3.661930099076, 3.661930099076, - 3.661930099076, 3.661930099076, - 3.661930099076, 3.661930099076, - 3.661930099076, 3.661930099076, - 3.661930099076, 3.661930099076, - 3.661930099076, 3.661930099076, - 3.661930099076, 3.661930099076, - 3.661930099076, 3.661930099076, - 3.661930099076, -0.04816277792592, - -0.04816277792592], - [10.5, 1.1, 1.1, 0.0, 0.0, 0.0, 0.0] - ), - (None, - None, - 'C[N+](C)(C)CC(=O)[O-]', - '[NH]=C(N)c1ccccc1', 1.0, 1.0, 1.0, @@ -2681,12 +3512,12 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 0.02664938770063, 0.02664938770063, 0.1489710476446, 0.1489710476446, 0.02960027280666, 0.02960027280666, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, - -0.0], - [10.5, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1] + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, -0.0, -0.0], + [10.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] ), (None, None, @@ -2696,10 +3527,11 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 0.0, 0.0, 0.0, + 0.0, 1.0, - [0.06697375586735, 0.06697375586735, + [1.256156174911, 1.256156174911, 0.06697375586735, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, -0.0, 0.0, 0.06697375586735, + 0.0, 0.0, -0.0, 0.0, 6.736762477654, 0.06697375586735, 0.06697375586735, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, @@ -2722,15 +3554,16 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, 3.661930099076, 3.661930099076, 3.661930099076, -0.04816277792592, -0.04816277792592], - [10.5, 1.1, 1.1, 0.0, 0.0, 0.0, 0.0] + [0.0, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1] ) ]) - def test_dihedrals_in_alchemical_topology(self, pdb1, pdb2, smiles1, smiles2, - fep_lambda, coul1_lambda, - coul2_lambda, vdw_lambda, - bonded_lambda, - golden_proper_constants, - golden_improper_constants): + def test_dihedrals_in_alchemical_topology(self, pdb1, pdb2, smiles1, + smiles2, fep_lambda, + coul_lambda,coul1_lambda, + coul2_lambda, vdw_lambda, + bonded_lambda, + golden_proper_constants, + golden_improper_constants): """ It validates the effects of lambda on atom parameters. """ @@ -2785,7 +3618,7 @@ def test_to_pdb(self): with tempfile.TemporaryDirectory() as tmpdir: with temporary_cd(tmpdir): output_path = os.path.join(tmpdir, 'alchemical_structure.pdb') - alchemizer.to_pdb(output_path) + alchemizer.hybrid_to_pdb(output_path) compare_files(reference, output_path) @@ -2809,7 +3642,5 @@ def test_rotamer_library_to_file(self): with tempfile.TemporaryDirectory() as tmpdir: with temporary_cd(tmpdir): - output_path = os.path.join(tmpdir, 'alchemical_structure.pdb') - alchemizer.to_pdb(output_path) - - compare_files(reference, output_path) + # TODO + pass diff --git a/peleffy/topology/__init__.py b/peleffy/topology/__init__.py index abba9c7e..edba3e2e 100644 --- a/peleffy/topology/__init__.py +++ b/peleffy/topology/__init__.py @@ -5,4 +5,4 @@ from .rotamer import RotamerLibrary from .conformer import BCEConformations from .mapper import Mapper -from .alchemy import Alchemizer +from .alchemistry import Alchemizer diff --git a/peleffy/topology/alchemy.py b/peleffy/topology/alchemistry.py similarity index 77% rename from peleffy/topology/alchemy.py rename to peleffy/topology/alchemistry.py index d5855087..ffa0d797 100644 --- a/peleffy/topology/alchemy.py +++ b/peleffy/topology/alchemistry.py @@ -157,9 +157,9 @@ def connections(self): """ return self._connections - def get_alchemical_topology(self, fep_lambda=None, coul1_lambda=None, - coul2_lambda=None, vdw_lambda=None, - bonded_lambda=None): + def get_alchemical_topology(self, fep_lambda=None, coul_lambda=None, + coul1_lambda=None, coul2_lambda=None, + vdw_lambda=None, bonded_lambda=None): """ Given a lambda, it returns an alchemical topology after combining both input topologies. @@ -170,24 +170,33 @@ def get_alchemical_topology(self, fep_lambda=None, coul1_lambda=None, The value to define an FEP lambda. This lambda affects all the parameters. It needs to be contained between 0 and 1. Default is None + coul_lambda : float + The value to define a general coulombic lambda. This lambda + only affects coulombic parameters of both molecules. It needs + to be contained between 0 and 1. It has precedence over + fep_lambda. Default is None coul1_lambda : float The value to define a coulombic lambda for exclusive atoms of molecule 1. This lambda only affects coulombic parameters of exclusive atoms of molecule 1. It needs to be contained - between 0 and 1. Default is None + between 0 and 1. It has precedence over coul_lambda or + fep_lambda. Default is None coul2_lambda : float The value to define a coulombic lambda for exclusive atoms of molecule 2. This lambda only affects coulombic parameters of exclusive atoms of molecule 2. It needs to be contained - between 0 and 1. Default is None + between 0 and 1. It has precedence over coul_lambda or + fep_lambda. Default is None vdw_lambda : float The value to define a vdw lambda. This lambda only affects van der Waals parameters. It needs to be contained - between 0 and 1. Default is None + between 0 and 1. It has precedence over fep_lambda. + Default is None bonded_lambda : float The value to define a coulombic lambda. This lambda only affects bonded parameters. It needs to be contained - between 0 and 1. Default is None + between 0 and 1. It has precedence over fep_lambda. + Default is None Returns ------- @@ -196,13 +205,14 @@ def get_alchemical_topology(self, fep_lambda=None, coul1_lambda=None, """ # Define lambdas fep_lambda = FEPLambda(fep_lambda) + coul_lambda = CoulombicLambda(coul_lambda) coul1_lambda = Coulombic1Lambda(coul1_lambda) coul2_lambda = Coulombic2Lambda(coul2_lambda) vdw_lambda = VanDerWaalsLambda(vdw_lambda) bonded_lambda = BondedLambda(bonded_lambda) - lambda_set = LambdaSet(fep_lambda, coul1_lambda, coul2_lambda, - vdw_lambda, bonded_lambda) + lambda_set = LambdaSet(fep_lambda, coul_lambda, coul1_lambda, + coul2_lambda, vdw_lambda, bonded_lambda) alchemical_topology = self.topology_from_lambda_set(lambda_set) @@ -228,6 +238,11 @@ def topology_from_lambda_set(self, lambda_set): alchemical_topology = deepcopy(self._joint_topology) + # Define mappers + mol1_mapped_atoms = [atom_pair[0] for atom_pair in self.mapping] + mol2_mapped_atoms = [atom_pair[1] for atom_pair in self.mapping] + mol1_to_mol2_map = dict(zip(mol1_mapped_atoms, mol2_mapped_atoms)) + for atom_idx, atom in enumerate(alchemical_topology.atoms): if atom_idx in self._exclusive_atoms: atom.apply_lambda(["sigma", "epsilon", "born_radius", @@ -249,28 +264,76 @@ def topology_from_lambda_set(self, lambda_set): lambda_set.get_lambda_for_coulomb2(), reverse=True) + if atom_idx in mol1_mapped_atoms: + mol2_idx = mol1_to_mol2_map[atom_idx] + mol2_atom = self.topology2.atoms[mol2_idx] + atom.apply_lambda(["sigma", "epsilon", "born_radius", + "SASA_radius", "nonpolar_gamma", + "nonpolar_alpha"], + lambda_set.get_lambda_for_vdw(), + reverse=False, + final_state=mol2_atom) + atom.apply_lambda(["charge"], + lambda_set.get_lambda_for_coulomb(), + reverse=False, + final_state=mol2_atom) + for bond_idx, bond in enumerate(alchemical_topology.bonds): if bond_idx in self._exclusive_bonds: - bond.apply_lambda(["spring_constant"], + bond.apply_lambda(["spring_constant", "eq_dist"], lambda_set.get_lambda_for_bonded(), reverse=False) if bond_idx in self._non_native_bonds: - bond.apply_lambda(["spring_constant"], + bond.apply_lambda(["spring_constant", "eq_dist"], lambda_set.get_lambda_for_bonded(), reverse=True) + atom1_idx = bond.atom1_idx + atom2_idx = bond.atom2_idx + if (atom1_idx in mol1_mapped_atoms and + atom2_idx in mol1_mapped_atoms): + mol_ids = (mol1_to_mol2_map[atom1_idx], + mol1_to_mol2_map[atom2_idx]) + + for mol2_bond in self.topology2.bonds: + if (mol2_bond.atom1_idx in mol_ids and + mol2_bond.atom2_idx in mol_ids): + bond.apply_lambda(["spring_constant", "eq_dist"], + lambda_set.get_lambda_for_bonded(), + reverse=False, + final_state=mol2_bond) + for angle_idx, angle in enumerate(alchemical_topology.angles): if angle_idx in self._exclusive_angles: - angle.apply_lambda(["spring_constant"], + angle.apply_lambda(["spring_constant", "eq_angle"], lambda_set.get_lambda_for_bonded(), reverse=False) if angle_idx in self._non_native_angles: - angle.apply_lambda(["spring_constant"], + angle.apply_lambda(["spring_constant", "eq_angle"], lambda_set.get_lambda_for_bonded(), reverse=True) + atom1_idx = angle.atom1_idx + atom2_idx = angle.atom2_idx + atom3_idx = angle.atom3_idx + if (atom1_idx in mol1_mapped_atoms and + atom2_idx in mol1_mapped_atoms and + atom3_idx in mol1_mapped_atoms): + mol_ids = (mol1_to_mol2_map[atom1_idx], + mol1_to_mol2_map[atom2_idx], + mol1_to_mol2_map[atom3_idx]) + + for mol2_angle in self.topology2.angles: + if (mol2_angle.atom1_idx in mol_ids and + mol2_angle.atom2_idx in mol_ids and + mol2_angle.atom3_idx in mol_ids): + angle.apply_lambda(["spring_constant", "eq_angle"], + lambda_set.get_lambda_for_bonded(), + reverse=False, + final_state=mol2_angle) + for proper_idx, proper in enumerate(alchemical_topology.propers): if proper_idx in self._exclusive_propers: proper.apply_lambda(["constant"], @@ -282,17 +345,65 @@ def topology_from_lambda_set(self, lambda_set): lambda_set.get_lambda_for_bonded(), reverse=True) + # TODO dihedrals cannot have mutual propers, all of them need to be non native or exclusive + atom1_idx = proper.atom1_idx + atom2_idx = proper.atom2_idx + atom3_idx = proper.atom3_idx + atom4_idx = proper.atom4_idx + if (atom1_idx in mol1_mapped_atoms and + atom2_idx in mol1_mapped_atoms and + atom3_idx in mol1_mapped_atoms and + atom4_idx in mol1_mapped_atoms): + mol_ids = (mol1_to_mol2_map[atom1_idx], + mol1_to_mol2_map[atom2_idx], + mol1_to_mol2_map[atom3_idx], + mol1_to_mol2_map[atom4_idx]) + + for mol2_proper in self.topology2.propers: + if (mol2_proper.atom1_idx in mol_ids and + mol2_proper.atom2_idx in mol_ids and + mol2_proper.atom3_idx in mol_ids and + mol2_proper.atom4_idx in mol_ids): + proper.apply_lambda(["constant"], + lambda_set.get_lambda_for_bonded(), + reverse=False, + final_state=mol2_proper) + for improper_idx, improper in enumerate(alchemical_topology.impropers): - if improper_idx in self._exclusive_propers: + if improper_idx in self._exclusive_impropers: improper.apply_lambda(["constant"], lambda_set.get_lambda_for_bonded(), reverse=False) - if improper_idx in self._non_native_propers: + if improper_idx in self._non_native_impropers: improper.apply_lambda(["constant"], lambda_set.get_lambda_for_bonded(), reverse=True) + # TODO dihedrals cannot have mutual propers, all of them need to be non native or exclusive + atom1_idx = improper.atom1_idx + atom2_idx = improper.atom2_idx + atom3_idx = improper.atom3_idx + atom4_idx = improper.atom4_idx + if (atom1_idx in mol1_mapped_atoms and + atom2_idx in mol1_mapped_atoms and + atom3_idx in mol1_mapped_atoms and + atom4_idx in mol1_mapped_atoms): + mol_ids = (mol1_to_mol2_map[atom1_idx], + mol1_to_mol2_map[atom2_idx], + mol1_to_mol2_map[atom3_idx], + mol1_to_mol2_map[atom4_idx]) + + for mol2_improper in self.topology2.impropers: + if (mol2_improper.atom1_idx in mol_ids and + mol2_improper.atom2_idx in mol_ids and + mol2_improper.atom3_idx in mol_ids and + mol2_improper.atom4_idx in mol_ids): + improper.apply_lambda(["constant"], + lambda_set.get_lambda_for_bonded(), + reverse=False, + final_state=mol2_improper) + return alchemical_topology def _join_topologies(self): @@ -419,6 +530,7 @@ def _join_topologies(self): new_bond.set_atom2_idx(mol2_to_alc_map[atom2_idx]) # Add new bond to the alchemical topology + non_native_bonds.append(new_index) joint_topology.add_bond(new_bond) # Add angles @@ -441,7 +553,7 @@ def _join_topologies(self): new_angle.set_atom2_idx(mol2_to_alc_map[atom2_idx]) new_angle.set_atom3_idx(mol2_to_alc_map[atom3_idx]) - # Add new bond to the alchemical topology + # Add new angle to the alchemical topology non_native_angles.append(new_index) joint_topology.add_angle(new_angle) @@ -468,7 +580,7 @@ def _join_topologies(self): new_proper.set_atom3_idx(mol2_to_alc_map[atom3_idx]) new_proper.set_atom4_idx(mol2_to_alc_map[atom4_idx]) - # Add new bond to the alchemical topology + # Add new proper to the alchemical topology non_native_propers.append(new_index) joint_topology.add_proper(new_proper) @@ -495,7 +607,7 @@ def _join_topologies(self): new_improper.set_atom3_idx(mol2_to_alc_map[atom3_idx]) new_improper.set_atom4_idx(mol2_to_alc_map[atom4_idx]) - # Add new bond to the alchemical topology + # Add new improper to the alchemical topology non_native_impropers.append(new_index) joint_topology.add_improper(new_improper) @@ -699,7 +811,7 @@ def _generate_alchemical_graph(self): return alchemical_graph, rotamers - def to_pdb(self, path): + def hybrid_to_pdb(self, path): """ Writes the alchemical molecule to a PDB file. @@ -720,8 +832,7 @@ def to_pdb(self, path): rdkit_wrapper.alchemical_combination(self.molecule1.rdkit_molecule, self.molecule2.rdkit_molecule, self.mapping, - self.connections, - self.mcs_mol) + self.connections) # Generate a dummy peleffy Molecule with the required information # to write it as a PDB file @@ -731,9 +842,53 @@ def to_pdb(self, path): rdkit_wrapper.to_pdb_file(molecule, path) + def molecule1_to_pdb(self, path): + """ + Writes the first molecule of the Alchemizer representation + to a PDB file. + + Parameters + ---------- + path : str + The path where to save the PDB file + """ + # Write it directly + self.molecule1.to_pdb_file(path) + + def molecule2_to_pdb(self, path): + """ + Writes the first molecule of the Alchemizer representation + to a PDB file. + + Parameters + ---------- + path : str + The path where to save the PDB file + """ + from peleffy.topology import Molecule + + # Align it to molecule 1 + from peleffy.utils.toolkits import RDKitToolkitWrapper + + rdkit_wrapper = RDKitToolkitWrapper() + mol2_aligned = \ + rdkit_wrapper.align_molecules(self.molecule1.rdkit_molecule, + self.molecule2.rdkit_molecule, + self.mapping) + + # Generate a dummy peleffy Molecule with the required information + # to write it as a PDB file + molecule = Molecule() + molecule._rdkit_molecule = mol2_aligned + molecule.set_tag(self.molecule2.tag) + + # Write it + rdkit_wrapper.to_pdb_file(molecule, path) + def rotamer_library_to_file(self, path, fep_lambda=None, - coul1_lambda=None, coul2_lambda=None, - vdw_lambda=None, bonded_lambda=None): + coul_lambda=None, coul1_lambda=None, + coul2_lambda=None, vdw_lambda=None, + bonded_lambda=None): """ It saves the alchemical rotamer library, which is the combination of the rotamer libraries of both molecules, to the path that @@ -745,24 +900,33 @@ def rotamer_library_to_file(self, path, fep_lambda=None, The value to define an FEP lambda. This lambda affects all the parameters. It needs to be contained between 0 and 1. Default is None + coul_lambda : float + The value to define a general coulombic lambda. This lambda + only affects coulombic parameters of both molecules. It needs + to be contained between 0 and 1. It has precedence over + fep_lambda. Default is None coul1_lambda : float The value to define a coulombic lambda for exclusive atoms of molecule 1. This lambda only affects coulombic parameters of exclusive atoms of molecule 1. It needs to be contained - between 0 and 1. Default is None + between 0 and 1. It has precedence over coul_lambda or + fep_lambda. Default is None coul2_lambda : float The value to define a coulombic lambda for exclusive atoms of molecule 2. This lambda only affects coulombic parameters of exclusive atoms of molecule 2. It needs to be contained - between 0 and 1. Default is None + between 0 and 1. It has precedence over coul_lambda or + fep_lambda. Default is None vdw_lambda : float The value to define a vdw lambda. This lambda only affects van der Waals parameters. It needs to be contained - between 0 and 1. Default is None + between 0 and 1. It has precedence over fep_lambda. + Default is None bonded_lambda : float The value to define a coulombic lambda. This lambda only affects bonded parameters. It needs to be contained - between 0 and 1. Default is None + between 0 and 1. It has precedence over fep_lambda. + Default is None Parameters ---------- @@ -771,22 +935,25 @@ def rotamer_library_to_file(self, path, fep_lambda=None, """ at_least_one = fep_lambda is not None or \ - coul1_lambda is not None or coul2_lambda is not None or \ - vdw_lambda is not None or bonded_lambda is not None + coul_lambda is not None or coul1_lambda is not None or \ + coul2_lambda is not None or vdw_lambda is not None or \ + bonded_lambda is not None # Define lambdas fep_lambda = FEPLambda(fep_lambda) + coul_lambda = CoulombicLambda(coul_lambda) coul1_lambda = Coulombic1Lambda(coul1_lambda) coul2_lambda = Coulombic2Lambda(coul2_lambda) vdw_lambda = VanDerWaalsLambda(vdw_lambda) bonded_lambda = BondedLambda(bonded_lambda) - lambda_set = LambdaSet(fep_lambda, coul1_lambda, coul2_lambda, - vdw_lambda, bonded_lambda) + lambda_set = LambdaSet(fep_lambda, coul_lambda, coul1_lambda, + coul2_lambda, vdw_lambda, bonded_lambda) if (at_least_one and lambda_set.get_lambda_for_bonded() == 0.0 and lambda_set.get_lambda_for_vdw() == 0.0 and + lambda_set.get_lambda_for_coulomb() == 0.0 and lambda_set.get_lambda_for_coulomb1() == 0.0 and lambda_set.get_lambda_for_coulomb2() == 0.0): rotamers = self.molecule1.rotamers @@ -795,6 +962,7 @@ def rotamer_library_to_file(self, path, fep_lambda=None, elif (at_least_one and lambda_set.get_lambda_for_bonded() == 1.0 and lambda_set.get_lambda_for_vdw() == 1.0 and + lambda_set.get_lambda_for_coulomb() == 1.0 and lambda_set.get_lambda_for_coulomb1() == 1.0 and lambda_set.get_lambda_for_coulomb2() == 1.0): rotamers = self.molecule2.rotamers @@ -808,7 +976,6 @@ def rotamer_library_to_file(self, path, fep_lambda=None, pdb_atom_names = [atom.PDB_name.replace(' ', '_',) for atom in self._joint_topology.atoms] molecule_tag = self._joint_topology.molecule.tag - mol2_mapped_atoms = [atom_pair[1] for atom_pair in self.mapping] with open(path, 'w') as file: file.write('rot assign res {} &\n'.format(molecule_tag)) @@ -919,6 +1086,13 @@ class FEPLambda(Lambda): """ _TYPE = "fep" +class CoulombicLambda(Lambda): + """ + It defines the CoulombicLambda class. It affects only coulombic + parameters involving both molecules. + """ + _TYPE = "coulombic" + class Coulombic1Lambda(Lambda): """ @@ -956,7 +1130,7 @@ class LambdaSet(object): It defines the LambdaSet class. """ - def __init__(self, fep_lambda, coul1_lambda, coul2_lambda, + def __init__(self, fep_lambda, coul_lambda, coul1_lambda, coul2_lambda, vdw_lambda, bonded_lambda): """ It initializes a LambdaSet object which stores all the different @@ -966,9 +1140,11 @@ def __init__(self, fep_lambda, coul1_lambda, coul2_lambda, ---------- fep_lambda : a peleffy.topology.alchemy.FEPLambda object The fep lambda - coul1_lambda : a peleffy.topology.alchemy.CoulombicLambda object + coul_lambda : a peleffy.topology.alchemy.CoulombicLambda object + The coulombic lambda for both molecules + coul1_lambda : a peleffy.topology.alchemy.Coulombic1Lambda object The coulombic lambda for exclusive atoms of molecule 1 - coul2_lambda : a peleffy.topology.alchemy.CoulombicLambda object + coul2_lambda : a peleffy.topology.alchemy.Coulombic2Lambda object The coulombic lambda for exclusive atoms of molecule 2 vdw_lambda : a peleffy.topology.alchemy.VanDerWaalsLambda object The van der Waals lambda @@ -979,22 +1155,26 @@ def __init__(self, fep_lambda, coul1_lambda, coul2_lambda, # Check parameters if not isinstance(fep_lambda, - peleffy.topology.alchemy.FEPLambda): + peleffy.topology.alchemistry.FEPLambda): raise TypeError('Invalid fep_lambda supplied to LambdaSet') + if not isinstance(coul_lambda, + peleffy.topology.alchemistry.CoulombicLambda): + raise TypeError('Invalid coul_lambda supplied to LambdaSet') if not isinstance(coul1_lambda, - peleffy.topology.alchemy.Coulombic1Lambda): + peleffy.topology.alchemistry.Coulombic1Lambda): raise TypeError('Invalid coul1_lambda supplied to LambdaSet') if not isinstance(coul2_lambda, - peleffy.topology.alchemy.Coulombic2Lambda): + peleffy.topology.alchemistry.Coulombic2Lambda): raise TypeError('Invalid coul2_lambda supplied to LambdaSet') if not isinstance(vdw_lambda, - peleffy.topology.alchemy.VanDerWaalsLambda): + peleffy.topology.alchemistry.VanDerWaalsLambda): raise TypeError('Invalid vdw_lambda supplied to LambdaSet') if not isinstance(bonded_lambda, - peleffy.topology.alchemy.BondedLambda): + peleffy.topology.alchemistry.BondedLambda): raise TypeError('Invalid bonded_lambda supplied to LambdaSet') self._fep_lambda = fep_lambda + self._coul_lambda = coul_lambda self._coul1_lambda = coul1_lambda self._coul2_lambda = coul2_lambda self._vdw_lambda = vdw_lambda @@ -1012,6 +1192,18 @@ def fep_lambda(self): """ return self._fep_lambda + @property + def coul_lambda(self): + """ + It returns the coul_lambda value. + + Returns + ------- + coul_lambda : float + The value of the coul_lambda + """ + return self._coul_lambda + @property def coul1_lambda(self): """ @@ -1080,6 +1272,28 @@ def get_lambda_for_vdw(self): return lambda_value + def get_lambda_for_coulomb(self): + """ + It returns the lambda to be applied on Coulomb parameters of + both molecules. + + Returns + ------- + lambda_value : float + The lambda value to be applied on Coulomb parameters of + both molecules + """ + if self.coul_lambda.is_set: + lambda_value = self.coul_lambda.value + + elif self.fep_lambda.is_set: + lambda_value = self.fep_lambda.value + + else: + lambda_value = 0.0 + + return lambda_value + def get_lambda_for_coulomb1(self): """ It returns the lambda to be applied on Coulomb parameters of @@ -1094,6 +1308,9 @@ def get_lambda_for_coulomb1(self): if self.coul1_lambda.is_set: lambda_value = self.coul1_lambda.value + elif self.coul_lambda.is_set: + lambda_value = self.coul_lambda.value + elif self.fep_lambda.is_set: lambda_value = self.fep_lambda.value @@ -1116,6 +1333,9 @@ def get_lambda_for_coulomb2(self): if self.coul2_lambda.is_set: lambda_value = self.coul2_lambda.value + elif self.coul_lambda.is_set: + lambda_value = self.coul_lambda.value + elif self.fep_lambda.is_set: lambda_value = self.fep_lambda.value diff --git a/peleffy/topology/elements.py b/peleffy/topology/elements.py index 9793914f..0d9c37a7 100644 --- a/peleffy/topology/elements.py +++ b/peleffy/topology/elements.py @@ -80,7 +80,7 @@ def n_writable_attrs(self): return len(self._writable_attrs) def apply_lambda(self, attributes_to_modify, lambda_value, - reverse=False): + reverse=False, final_state=None): """ Given a lambda value, it modifies a set of attributes of this topological element. A lambda equal to 0 will keep the @@ -99,11 +99,27 @@ def apply_lambda(self, attributes_to_modify, lambda_value, reverse : bool When set to true the effects of lambda will be the opposite + final_state : a peleffy.topology.topology._TopologyElement object + The topology element that represents the final state when + lambda equals 1.0. Default is None, which means that the + final state is not defined and therefore the topological + element will disappear or will start from scratch """ from peleffy.utils import Logger logger = Logger() + # Check final_state + if final_state is not None: + if type(final_state) != type(self): + logger.error([f'Final state must belong to the ' + + f'same topological element as ' + + f'the element that wants to be ' + + f'modified with a lambda value. ' + + f'It will not be changed.']) + + return + if not reverse: lambda_value = 1.0 - lambda_value @@ -119,6 +135,12 @@ def apply_lambda(self, attributes_to_modify, lambda_value, if value is not None: value = value * lambda_value + if final_state is not None: + final_value = getattr(final_state, '_' + attribute) + + if final_value is not None: + value = value + (1.0 - lambda_value) * final_value + setattr(self, '_' + attribute, value) def __iter__(self): @@ -612,7 +634,7 @@ class Bond(_TopologyElement): _name = 'Bond' _writable_attrs = ['atom1_idx', 'atom2_idx', 'spring_constant', 'eq_dist'] - _lambda_changeable = ['spring_constant'] + _lambda_changeable = ['spring_constant', 'eq_dist'] def __init__(self, index=-1, atom1_idx=None, atom2_idx=None, spring_constant=None, eq_dist=None): @@ -740,7 +762,7 @@ class Angle(_TopologyElement): _name = 'Angle' _writable_attrs = ['atom1_idx', 'atom2_idx', 'atom3_idx', 'spring_constant', 'eq_angle'] - _lambda_changeable = ['spring_constant'] + _lambda_changeable = ['spring_constant', 'eq_angle'] def __init__(self, index=-1, atom1_idx=None, atom2_idx=None, atom3_idx=None, spring_constant=None, eq_angle=None): diff --git a/peleffy/utils/toolkits.py b/peleffy/utils/toolkits.py index 0524580f..14c85863 100644 --- a/peleffy/utils/toolkits.py +++ b/peleffy/utils/toolkits.py @@ -853,8 +853,45 @@ def draw_mapping(self, molecule1, molecule2, mcs_mol, return image + def align_molecules(self, mol1, mol2, atom_mapping=None): + """ + It aligns the two molecules that are given, taking the first + one as reference. + + Parameters + ---------- + mol1 : an RDKit.molecule object + The first molecule to use as reference in the alignment + mol2 : an RDKit.molecule object + The second molecule which will be aligned over the first one + atom_mapping : list[tuple[int, int]] + The list containing the mapping between atoms of both + molecules. First index of each pair belongs to molecule + 1, the second one belongs to molecule 2 + + Returns + ------- + aligned_mol2 : an RDKit.molecule object + The resulting mol2 after the alignment. It is a new copy + of the original mol2 + """ + from copy import deepcopy + from rdkit import Chem + + # Make a copy of molecule 2 + mol2 = deepcopy(mol2) + + # Generate inverse mapping + inverse_mapping = [(idxs[1], idxs[0]) for idxs in atom_mapping] + + # Align molecule 2 to molecule 1 + Chem.rdMolAlign.AlignMol(mol2, mol1, + atomMap=inverse_mapping) + + return mol2 + def alchemical_combination(self, mol1, mol2, atom_mapping, - connections, mcs_mol): + connections): """ Given two molecules, it return the alchemical combination of them. Both molecules are superposed taking the first one @@ -874,9 +911,6 @@ def alchemical_combination(self, mol1, mol2, atom_mapping, connections : list[tuple[int, int]] The list of connections between molecule 1 and non native atoms of molecule 2 - mcs_mol : an RDKit.molecule object - The molecule representinc the MCS substructure of both - molecules Returns ------- @@ -885,20 +919,18 @@ def alchemical_combination(self, mol1, mol2, atom_mapping, molecules """ from rdkit import Chem - from copy import deepcopy - # Make a copy of molecule 2 - mol2 = deepcopy(mol2) + # Align mol2 to mol1 + mol2_aligned = self.align_molecules(mol1, mol2, atom_mapping) - # Generate inverse mapping - inverse_mapping = [(idxs[1], idxs[0]) for idxs in atom_mapping] + # Remove common substructure from molecule 2 + mol2_truncated = Chem.EditableMol(mol2_aligned) + atom_ids_to_remove = [pair[1] for pair in atom_mapping] - # Align molecule 2 to molecule 1 - Chem.rdMolAlign.AlignMol(mol2, mol1, - atomMap=inverse_mapping) + for atom in sorted(atom_ids_to_remove, reverse=True): + mol2_truncated.RemoveAtom(atom) - # Remove common substructure from molecule 2 - mol2_truncated = Chem.DeleteSubstructs(mol2, mcs_mol) + mol2_truncated = mol2_truncated.GetMol() # Combine molecule1 with truncated molecule 2 mol_combo = Chem.CombineMols(mol1, @@ -917,10 +949,6 @@ def alchemical_combination(self, mol1, mol2, atom_mapping, pdb_info.SetResidueNumber(1) pdb_info.SetChainId('L') pdb_info.SetIsHeteroAtom(True) - for atom in mol_combo.GetAtoms(): - atom_name = atom.GetPDBResidueInfo().GetName() - pdb_info.SetName(atom_name) - atom.SetPDBResidueInfo(pdb_info) return mol_combo From 18ff47df445e6837fecc99dea0d164196089df8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 29 Sep 2021 17:25:11 +0200 Subject: [PATCH 09/29] Add AMBER compatibility --- peleffy/data/tests/OPLS_etlz_amber | 40 ++++++++++ peleffy/data/tests/OPLS_malz_amber | 71 +++++++++++++++++ peleffy/data/tests/OPLS_metz_amber | 31 ++++++++ peleffy/data/tests/etlz_amber | 40 ++++++++++ peleffy/data/tests/malz_amber | 78 ++++++++++++++++++ peleffy/data/tests/metz_amber | 31 ++++++++ peleffy/template/impact.py | 41 +++++++++- peleffy/tests/test_templates.py | 123 ++++++++++++++++++++++++++++- 8 files changed, 451 insertions(+), 4 deletions(-) create mode 100644 peleffy/data/tests/OPLS_etlz_amber create mode 100644 peleffy/data/tests/OPLS_malz_amber create mode 100644 peleffy/data/tests/OPLS_metz_amber create mode 100644 peleffy/data/tests/etlz_amber create mode 100644 peleffy/data/tests/malz_amber create mode 100644 peleffy/data/tests/metz_amber diff --git a/peleffy/data/tests/OPLS_etlz_amber b/peleffy/data/tests/OPLS_etlz_amber new file mode 100644 index 00000000..fa124ed6 --- /dev/null +++ b/peleffy/data/tests/OPLS_etlz_amber @@ -0,0 +1,40 @@ +* LIGAND DATABASE FILE (OPLS2005) +* File generated with peleffy-1.3.4+0.ge91a3a1.dirty +* Compatible with PELE's AMBER implementation +* +ETL 6 5 6 6 0 + 1 0 M CM _C1_ 0 0.375358 58.428399 -18.105831 + 2 1 M CM _C2_ 0 1.317567 175.445160 81.849744 + 3 1 M HC _H1_ 0 1.093548 164.351605 72.102200 + 4 1 M HC _H2_ 0 1.085214 159.989210 -104.040125 + 5 2 M HC _H3_ 0 1.077760 147.312594 -6.798446 + 6 2 M HC _H4_ 0 1.073941 145.566151 173.153836 +NBON + 1 1.9924 0.0760 -0.230000 2.0020 1.7750 0.023028004 -0.852763146 + 2 1.9924 0.0760 -0.230000 2.0020 1.7750 0.023028004 -0.852763146 + 3 1.3582 0.0300 0.115000 1.4250 1.2500 0.008598240 0.268726247 + 4 1.3582 0.0300 0.115000 1.4250 1.2500 0.008598240 0.268726247 + 5 1.3582 0.0300 0.115000 1.4250 1.2500 0.008598240 0.268726247 + 6 1.3582 0.0300 0.115000 1.4250 1.2500 0.008598240 0.268726247 +BOND + 1 2 549.000 1.340 + 1 3 340.000 1.080 + 1 4 340.000 1.080 + 2 5 340.000 1.080 + 2 6 340.000 1.080 +THET + 2 1 3 35.00000 120.00000 + 2 1 4 35.00000 120.00000 + 3 1 4 35.00000 117.00000 + 1 2 5 35.00000 120.00000 + 1 2 6 35.00000 120.00000 + 5 2 6 35.00000 117.00000 +PHI + 3 1 2 5 7.00000 -1.0 2.0 + 3 1 2 6 7.00000 -1.0 2.0 + 4 1 2 5 7.00000 -1.0 2.0 + 4 1 2 6 7.00000 -1.0 2.0 +IPHI + 3 4 1 2 15.00000 -1.0 2.0 + 5 6 2 1 15.00000 -1.0 2.0 +END diff --git a/peleffy/data/tests/OPLS_malz_amber b/peleffy/data/tests/OPLS_malz_amber new file mode 100644 index 00000000..71e2ee88 --- /dev/null +++ b/peleffy/data/tests/OPLS_malz_amber @@ -0,0 +1,71 @@ +* LIGAND DATABASE FILE (OPLS2005) +* File generated with peleffy-1.3.4+0.ge91a3a1.dirty +* Compatible with PELE's AMBER implementation +* +UNL 10 9 13 18 0 + 1 0 M CT _C2_ 0 1.351681 122.976486 -1.401441 + 2 1 M HC _H1_ 0 1.115174 129.960154 -125.800180 + 3 1 M HC _H2_ 0 1.104475 141.443968 127.264070 + 4 1 S CO3 _C1_ 0 1.471015 154.513939 -8.710222 + 5 1 S C _C3_ 0 1.471246 113.290420 32.282884 + 6 4 S O2Z _O1_ 0 1.248148 147.066621 -42.333451 + 7 4 S O2Z _O2_ 0 1.378430 148.634167 137.606235 + 8 5 S OH _O3_ 0 1.400505 150.092585 34.077933 + 9 5 S O _O4_ 0 1.239650 147.366131 -145.846157 + 10 8 S HO _H3_ 0 1.010543 142.604914 -86.560488 +NBON + 1 1.9643 0.0660 -0.220000 1.9750 1.7500 0.005000000 -0.741685710 + 2 1.4031 0.0300 0.060000 1.4250 1.2500 0.008598240 0.268726247 + 3 1.4031 0.0300 0.060000 1.4250 1.2500 0.008598240 0.268726247 + 4 2.1046 0.1050 0.700000 2.1120 1.8750 0.001000000 -0.126889456 + 5 2.1046 0.1050 0.520000 2.1120 1.8750 0.001000000 -0.126889456 + 6 1.6612 0.2100 -0.800000 1.8650 1.6500 0.020881802 -0.347800883 + 7 1.6612 0.2100 -0.800000 1.6780 1.4800 0.001000000 -0.126889456 + 8 1.6837 0.1700 -0.530000 1.7660 1.5600 0.021068034 -0.322181743 + 9 1.6612 0.2100 -0.440000 1.6780 1.4800 0.001000000 -0.126889456 + 10 0.2806 0.0300 0.450000 0.9960 0.8600 0.030040813 -0.651083722 +BOND + 6 4 656.000 1.250 + 4 7 656.000 1.250 + 4 1 317.000 1.522 + 1 5 317.000 1.522 + 1 2 340.000 1.090 + 1 3 340.000 1.090 + 5 8 450.000 1.364 + 5 9 570.000 1.229 + 8 10 553.000 0.945 +THET + 6 4 7 80.00000 126.00000 + 6 4 1 70.00000 117.00000 + 7 4 1 70.00000 117.00000 + 4 1 5 75.00000 103.47400 + 4 1 2 35.00000 109.50000 + 4 1 3 35.00000 109.50000 + 5 1 2 35.00000 109.50000 + 5 1 3 35.00000 109.50000 + 2 1 3 33.00000 107.80000 + 1 5 8 70.00000 108.00000 + 1 5 9 80.00000 120.40000 + 8 5 9 100.00000 123.32000 + 5 8 10 35.00000 113.00000 +PHI + 6 4 1 5 0.30150 -1.0 2.0 + 6 4 1 2 0.00000 1.0 1.0 + 6 4 1 3 0.00000 1.0 1.0 + 7 4 1 5 0.30150 -1.0 2.0 + 7 4 1 2 0.00000 1.0 1.0 + 7 4 1 3 0.00000 1.0 1.0 + 4 1 5 8 0.40000 1.0 3.0 + 4 1 5 9 0.30150 -1.0 2.0 + 2 1 5 8 0.00000 1.0 1.0 + 2 1 5 9 0.00000 1.0 1.0 + 3 1 5 8 0.00000 1.0 1.0 + 3 1 5 9 0.00000 1.0 1.0 + 1 5 8 10 1.50000 1.0 1.0 + 1 5 8 10 2.45000 -1.0 2.0 + 9 5 8 10 0.92200 1.0 1.0 + 9 5 8 10 3.74100 -1.0 2.0 +IPHI + 7 1 4 6 10.50000 -1.0 2.0 + 1 8 5 9 10.50000 -1.0 2.0 +END diff --git a/peleffy/data/tests/OPLS_metz_amber b/peleffy/data/tests/OPLS_metz_amber new file mode 100644 index 00000000..6e39d40b --- /dev/null +++ b/peleffy/data/tests/OPLS_metz_amber @@ -0,0 +1,31 @@ +* LIGAND DATABASE FILE (OPLS2005) +* File generated with peleffy-1.3.4+0.ge91a3a1.dirty +* Compatible with PELE's AMBER implementation +* +UNK 5 4 6 0 0 + 1 0 M CT _C1_ 0 0.992114 89.496550 -1.226278 + 2 1 M HC _H1_ 0 1.107235 124.427757 130.975534 + 3 1 M HC _H2_ 0 1.097230 121.728887 -47.361737 + 4 1 M HC _H3_ 0 1.106868 155.277863 -139.041454 + 5 1 M HC _H4_ 0 1.107506 154.421955 40.055461 +NBON + 1 1.9643 0.0660 -0.240000 1.9750 1.7500 0.005000000 -0.741685710 + 2 1.4031 0.0300 0.060000 1.4250 1.2500 0.008598240 0.268726247 + 3 1.4031 0.0300 0.060000 1.4250 1.2500 0.008598240 0.268726247 + 4 1.4031 0.0300 0.060000 1.4250 1.2500 0.008598240 0.268726247 + 5 1.4031 0.0300 0.060000 1.4250 1.2500 0.008598240 0.268726247 +BOND + 1 2 340.000 1.090 + 1 3 340.000 1.090 + 1 4 340.000 1.090 + 1 5 340.000 1.090 +THET + 2 1 3 33.00000 107.80000 + 2 1 4 33.00000 107.80000 + 2 1 5 33.00000 107.80000 + 3 1 4 33.00000 107.80000 + 3 1 5 33.00000 107.80000 + 4 1 5 33.00000 107.80000 +PHI +IPHI +END diff --git a/peleffy/data/tests/etlz_amber b/peleffy/data/tests/etlz_amber new file mode 100644 index 00000000..8939ab40 --- /dev/null +++ b/peleffy/data/tests/etlz_amber @@ -0,0 +1,40 @@ +* LIGAND DATABASE FILE (openff_unconstrained-1.2.1.offxml) +* File generated with peleffy-1.3.4+0.ge91a3a1.dirty +* Compatible with PELE's AMBER implementation +* +ETL 6 5 6 6 0 + 1 0 M OFFT _C1_ 0 0.375358 58.428399 -18.105831 + 2 1 M OFFT _C2_ 0 1.317567 175.445160 81.849744 + 3 1 M OFFT _H1_ 0 1.093548 164.351605 72.102200 + 4 1 M OFFT _H2_ 0 1.085214 159.989210 -104.040125 + 5 2 M OFFT _H3_ 0 1.077760 147.312594 -6.798446 + 6 2 M OFFT _H4_ 0 1.073941 145.566151 173.153836 +NBON + 1 1.9080 0.0860 -0.218000 0.0000 1.6998 0.000000000 0.000000000 + 2 1.9080 0.0860 -0.218000 0.0000 1.6998 0.000000000 0.000000000 + 3 1.4590 0.0150 0.109000 0.0000 1.2998 0.000000000 0.000000000 + 4 1.4590 0.0150 0.109000 0.0000 1.2998 0.000000000 0.000000000 + 5 1.4590 0.0150 0.109000 0.0000 1.2998 0.000000000 0.000000000 + 6 1.4590 0.0150 0.109000 0.0000 1.2998 0.000000000 0.000000000 +BOND + 1 2 404.839 1.372 + 1 3 404.208 1.086 + 1 4 404.208 1.086 + 2 5 404.208 1.086 + 2 6 404.208 1.086 +THET + 1 2 5 34.20296 133.13398 + 1 2 6 34.20296 133.13398 + 2 1 3 34.20296 133.13398 + 2 1 4 34.20296 133.13398 + 3 1 4 27.86110 134.06420 + 5 2 6 27.86110 134.06420 +PHI + 3 1 2 5 5.37602 -1.0 2.0 + 3 1 2 6 5.37602 -1.0 2.0 + 4 1 2 5 5.37602 -1.0 2.0 + 4 1 2 6 5.37602 -1.0 2.0 +IPHI + 1 2 5 6 1.10000 -1.0 2.0 + 2 1 3 4 1.10000 -1.0 2.0 +END diff --git a/peleffy/data/tests/malz_amber b/peleffy/data/tests/malz_amber new file mode 100644 index 00000000..be82213e --- /dev/null +++ b/peleffy/data/tests/malz_amber @@ -0,0 +1,78 @@ +* LIGAND DATABASE FILE (openff_unconstrained-1.2.1.offxml) +* File generated with peleffy-1.3.4+0.ge91a3a1.dirty +* Compatible with PELE's AMBER implementation +* +UNL 10 9 13 25 0 + 1 0 M OFFT _C2_ 0 1.351681 122.976486 -1.401441 + 2 1 M OFFT _H1_ 0 1.115174 129.960154 -125.800180 + 3 1 M OFFT _H2_ 0 1.104475 141.443968 127.264070 + 4 1 S OFFT _C1_ 0 1.471015 154.513939 -8.710222 + 5 1 S OFFT _C3_ 0 1.471246 113.290420 32.282884 + 6 4 S OFFT _O1_ 0 1.248148 147.066621 -42.333451 + 7 4 S OFFT _O2_ 0 1.378430 148.634167 137.606235 + 8 5 S OFFT _O3_ 0 1.400505 150.092585 34.077933 + 9 5 S OFFT _O4_ 0 1.239650 147.366131 -145.846157 + 10 8 S OFFT _H3_ 0 1.010543 142.604914 -86.560488 +NBON + 1 1.9080 0.1094 -0.269400 0.0000 1.6998 0.000000000 0.000000000 + 2 1.4870 0.0157 0.061700 0.0000 1.3248 0.000000000 0.000000000 + 3 1.4870 0.0157 0.061700 0.0000 1.3248 0.000000000 0.000000000 + 4 1.9080 0.0860 0.934600 0.0000 1.6998 0.000000000 0.000000000 + 5 1.9080 0.0860 0.681100 0.0000 1.6998 0.000000000 0.000000000 + 6 1.6612 0.2100 -0.824300 0.0000 1.4800 0.000000000 0.000000000 + 7 1.6612 0.2100 -0.824300 0.0000 1.4800 0.000000000 0.000000000 + 8 1.7210 0.2104 -0.624100 0.0000 1.5332 0.000000000 0.000000000 + 9 1.6612 0.2100 -0.612000 0.0000 1.4800 0.000000000 0.000000000 + 10 0.3000 0.0001 0.414000 0.0000 0.2673 0.000000000 0.000000000 +BOND + 6 4 580.529 1.258 + 4 7 580.529 1.258 + 4 1 332.575 1.524 + 1 5 332.575 1.524 + 1 2 376.894 1.094 + 1 3 376.894 1.094 + 5 8 387.876 1.369 + 5 9 608.329 1.225 + 8 10 556.995 0.974 +THET + 6 4 7 205.49111 129.05414 + 6 4 1 78.67881 128.27719 + 4 1 5 50.00416 110.81136 + 4 1 2 50.00416 110.81136 + 4 1 3 50.00416 110.81136 + 7 4 1 78.67881 128.27719 + 1 5 8 78.67881 128.27719 + 1 5 9 78.67881 128.27719 + 5 1 2 50.00416 110.81136 + 5 1 3 50.00416 110.81136 + 5 8 10 63.95595 110.94623 + 8 5 9 205.49111 129.05414 + 2 1 3 33.78876 110.24686 +PHI + 6 4 1 5 -0.39977 1.0 2.0 + 6 4 1 2 0.51072 1.0 1.0 + 6 4 1 3 0.51072 1.0 1.0 + 4 1 5 8 0.14940 1.0 3.0 + 4 1 5 9 -0.39977 1.0 2.0 + 7 4 1 5 0.14940 1.0 3.0 + 7 4 1 2 0.14940 1.0 3.0 + 7 4 1 3 0.14940 1.0 3.0 + 1 5 8 10 2.52911 -1.0 2.0 + 8 5 1 2 0.14940 1.0 3.0 + 8 5 1 3 0.14940 1.0 3.0 + 9 5 1 2 0.51072 1.0 1.0 + 9 5 1 3 0.51072 1.0 1.0 + 9 5 8 10 2.23793 -1.0 2.0 + 6 4 1 2 -0.02752 1.0 2.0 + 6 4 1 3 -0.02752 1.0 2.0 + 9 5 1 2 -0.02752 1.0 2.0 + 9 5 1 3 -0.02752 1.0 2.0 + 9 5 8 10 1.23729 1.0 1.0 + 6 4 1 2 -0.10575 -1.0 3.0 + 6 4 1 3 -0.10575 -1.0 3.0 + 9 5 1 2 -0.10575 -1.0 3.0 + 9 5 1 3 -0.10575 -1.0 3.0 +IPHI + 6 4 7 1 10.50000 -1.0 2.0 + 1 5 8 9 10.50000 -1.0 2.0 +END diff --git a/peleffy/data/tests/metz_amber b/peleffy/data/tests/metz_amber new file mode 100644 index 00000000..ead98a58 --- /dev/null +++ b/peleffy/data/tests/metz_amber @@ -0,0 +1,31 @@ +* LIGAND DATABASE FILE (openff_unconstrained-1.2.1.offxml) +* File generated with peleffy-1.3.4+0.ge91a3a1.dirty +* Compatible with PELE's AMBER implementation +* +UNK 5 4 6 0 0 + 1 0 M OFFT _C1_ 0 0.992114 89.496550 -1.226278 + 2 1 M OFFT _H1_ 0 1.107235 124.427757 130.975534 + 3 1 M OFFT _H2_ 0 1.097230 121.728887 -47.361737 + 4 1 M OFFT _H3_ 0 1.106868 155.277863 -139.041454 + 5 1 M OFFT _H4_ 0 1.107506 154.421955 40.055461 +NBON + 1 1.9080 0.1094 -0.108800 0.0000 1.6998 0.000000000 0.000000000 + 2 1.4870 0.0157 0.026700 0.0000 1.3248 0.000000000 0.000000000 + 3 1.4870 0.0157 0.026700 0.0000 1.3248 0.000000000 0.000000000 + 4 1.4870 0.0157 0.026700 0.0000 1.3248 0.000000000 0.000000000 + 5 1.4870 0.0157 0.026700 0.0000 1.3248 0.000000000 0.000000000 +BOND + 1 2 376.894 1.094 + 1 3 376.894 1.094 + 1 4 376.894 1.094 + 1 5 376.894 1.094 +THET + 2 1 3 33.78876 110.24686 + 2 1 4 33.78876 110.24686 + 2 1 5 33.78876 110.24686 + 3 1 4 33.78876 110.24686 + 3 1 5 33.78876 110.24686 + 4 1 5 33.78876 110.24686 +PHI +IPHI +END diff --git a/peleffy/template/impact.py b/peleffy/template/impact.py index ea929979..b7e31e32 100644 --- a/peleffy/template/impact.py +++ b/peleffy/template/impact.py @@ -17,7 +17,9 @@ class Impact(object): template. """ - def __init__(self, topology): + H_6R_OF_2 = 0.5612310241546865 # The half of the sixth root of 2 + + def __init__(self, topology, for_amber=False): """ Initializes an Impact object. @@ -26,6 +28,11 @@ def __init__(self, topology): topology : a Topology object The molecular topology representation to write as a Impact template + for_amber : bool + Whether to save an Impact file compatible with PELE's + Amber force field implementation or not. Default is + False which will create an Impact file compatible + with OPLS2005 Examples -------- @@ -38,7 +45,7 @@ def __init__(self, topology): >>> from peleffy.forcefield import OpenForceField - >>> openff = OpenForceField('openff_unconstrained-1.2.1.offxml') + >>> openff = OpenForceField('openff_unconstrained-2.0.0.offxml') >>> parameters = openff.parameterize(molecule) >>> from peleffy.topology import Topology @@ -50,6 +57,26 @@ def __init__(self, topology): >>> impact = Impact(topology) >>> impact.to_file('molz') + Write the Impact template of a peleffy's molecule compatible with PELE's Amber forcefield implementation + + >>> from peleffy.topology import Molecule + + >>> molecule = Molecule('molecule.pdb') + + >>> from peleffy.forcefield import OpenForceField + + >>> openff = OpenForceField('openff_unconstrained-2.0.0.offxml') + >>> parameters = openff.parameterize(molecule) + + >>> from peleffy.topology import Topology + + >>> topology = Topology(molecule, parameters) + + >>> from peleffy.template import Impact + + >>> impact = Impact(topology, for_amber=True) + >>> impact.to_file('molz') + """ import peleffy @@ -59,6 +86,7 @@ def __init__(self, topology): isinstance(topology, peleffy.topology.topology.Topology)): raise TypeError('Invalid input molecule for Impact template') + self._for_amber = for_amber self._topology = deepcopy(topology) self._molecule = self._topology.molecule self._sort() @@ -231,6 +259,10 @@ def _write_header(self, file): file.write('\n') file.write('* File generated with peleffy-{}\n'.format( peleffy.__version__)) + if self._for_amber: + file.write('* Compatible with PELE\'s AMBER implementation\n') + else: + file.write('* Compatible with PELE\'s OPLS implementation\n') file.write('*\n') def _write_resx(self, file): @@ -303,7 +335,10 @@ def _write_nbon(self, file): file.write('{:5d}'.format(w_atom.index)) file.write(' ') # Sigma - file.write('{: 8.4f}'.format(w_atom.sigma)) + if self._for_amber: + file.write('{: 8.4f}'.format(w_atom.sigma * self.H_6R_OF_2)) + else: + file.write('{: 8.4f}'.format(w_atom.sigma)) file.write(' ') # Epsilon file.write('{: 8.4f}'.format(w_atom.epsilon)) diff --git a/peleffy/tests/test_templates.py b/peleffy/tests/test_templates.py index 1fbb0547..8d17ad6e 100644 --- a/peleffy/tests/test_templates.py +++ b/peleffy/tests/test_templates.py @@ -100,9 +100,65 @@ def test_writer_OFF(self): # Compare the reference template and the generated template compare_files(file1=TEMPLATE_ETLZ, file2='etlz') + def test_writer_OFF_for_AMBER(self): + """ + It tests the writer attribute of the Impact class using OFF + to parameterize and the compatibility mode for PELE's AMBER + implementation. + """ + + TEMPLATE_METZ = get_data_file_path('tests/metz_amber') + TEMPLATE_MATZ = get_data_file_path('tests/malz_amber') + TEMPLATE_ETLZ = get_data_file_path('tests/etlz_amber') + + with tempfile.TemporaryDirectory() as tmpdir: + with temporary_cd(tmpdir): + # Generates the template for methane + pdb_path = get_data_file_path('ligands/methane.pdb') + molecule = Molecule(pdb_path) + openff = OpenForceField(self.OPENFF_FORCEFIELD) + parameters = openff.parameterize(molecule) + topology = Topology(molecule, parameters) + + # Generates the impact template for methane + impact = Impact(topology, for_amber=True) + impact.to_file('metz') + + # Compare the reference template and the generated template + compare_files(file1=TEMPLATE_METZ, file2='metz') + + # Generates the template for malonate + pdb_path = get_data_file_path('ligands/malonate.pdb') + molecule = Molecule(pdb_path) + openff = OpenForceField(self.OPENFF_FORCEFIELD) + parameters = openff.parameterize(molecule) + topology = Topology(molecule, parameters) + + # Generates the impact template for malonate + impact = Impact(topology, for_amber=True) + impact.to_file('malz') + + # Compare the reference template and the generated template + compare_files(file1=TEMPLATE_MATZ, file2='malz') + + # Generates the template for ethylene + pdb_path = get_data_file_path('ligands/ethylene.pdb') + molecule = Molecule(pdb_path, tag='ETL') # Note that in this case we are assigning a tag to the molecule which will be used in the Impact template + openff = OpenForceField(self.OPENFF_FORCEFIELD) + parameters = openff.parameterize(molecule) + topology = Topology(molecule, parameters) + + # Generates the impact template for ethylene + impact = Impact(topology, for_amber=True) + impact.to_file('etlz') + + # Compare the reference template and the generated template + compare_files(file1=TEMPLATE_ETLZ, file2='etlz') + def test_writer_OPLS(self): """ - It tests the writer attribute of the Impact class using OPLS to parameterize. + It tests the writer attribute of the Impact class using OPLS + to parameterize. """ from .utils import parameterize_opls2005 @@ -163,6 +219,71 @@ def test_writer_OPLS(self): # Compare the reference template and the generated template compare_files(file1=TEMPLATE_ETLZ_OPLS, file2='etlz') + def test_writer_OPLS_for_AMBER(self): + """ + It tests the writer attribute of the Impact class using OPLS + to parameterize and the compatibility mode for PELE's AMBER + implementation. + """ + from .utils import parameterize_opls2005 + + TEMPLATE_METZ_OPLS = get_data_file_path('tests/OPLS_metz_amber') + TEMPLATE_MALZ_OPLS = get_data_file_path('tests/OPLS_malz_amber') + TEMPLATE_ETLZ_OPLS = get_data_file_path('tests/OPLS_etlz_amber') + + with tempfile.TemporaryDirectory() as tmpdir: + with temporary_cd(tmpdir): + # Generates the template for methane using OPLS + opls2005 = OPLS2005ForceField() + pdb_path = get_data_file_path('ligands/methane.pdb') + molecule = Molecule(pdb_path) + ffld_file = get_data_file_path('tests/MET_ffld_output.txt') + parameters = parameterize_opls2005(opls2005, + molecule, + ffld_file) + topology = Topology(molecule, parameters) + + # Generates the impact template for methane + impact = Impact(topology, for_amber=True) + impact.to_file('metz') + + # Compare the reference template and the generated template + compare_files(file1=TEMPLATE_METZ_OPLS, file2='metz') + + # Generates the template for malonate using OPLS + opls2005 = OPLS2005ForceField() + pdb_path = get_data_file_path('ligands/malonate.pdb') + molecule = Molecule(pdb_path) + ffld_file = get_data_file_path('tests/MAL_ffld_output.txt') + parameters = parameterize_opls2005(opls2005, + molecule, + ffld_file) + topology = Topology(molecule, parameters) + + # Generates the impact template for malonate + impact = Impact(topology, for_amber=True) + impact.to_file('malz') + + # Compare the reference template and the generated template + compare_files(file1=TEMPLATE_MALZ_OPLS, file2='malz') + + # Generates the template for ethylene using OPLS + opls2005 = OPLS2005ForceField() + pdb_path = get_data_file_path('ligands/ethylene.pdb') + molecule = Molecule(pdb_path, tag='ETL') + ffld_file = get_data_file_path('tests/ETL_ffld_output.txt') + parameters = parameterize_opls2005(opls2005, + molecule, + ffld_file) + topology = Topology(molecule, parameters) + + # Generates the impact template for ethylene + impact = Impact(topology, for_amber=True) + impact.to_file('etlz') + + # Compare the reference template and the generated template + compare_files(file1=TEMPLATE_ETLZ_OPLS, file2='etlz') + def test_get_absolute_parent_atom(self): """ It tests the _get_absolute_parent_atom method used in the building From 59cf36484c9c20f7254c6ba9f6fdde84edce36c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 29 Sep 2021 18:30:22 +0200 Subject: [PATCH 10/29] Add for_amber option in main --- peleffy/main.py | 50 ++++++++++++++++++++------------- peleffy/tests/test_main.py | 57 +++++++++++++++++++++++++++++++++----- 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/peleffy/main.py b/peleffy/main.py index 6a30e1aa..673adf76 100644 --- a/peleffy/main.py +++ b/peleffy/main.py @@ -45,28 +45,28 @@ def parse_args(args): parser.add_argument("pdb_file", metavar="PDB FILE", type=str, help="Path PDB file to parameterize") parser.add_argument("-f", "--forcefield", metavar="NAME", - type=str, help="OpenForceField's forcefield name. " - + "Default is " + str(DEFAULT_OFF_FORCEFIELD), + type=str, help="OpenForceField's forcefield name. " + + "Default is " + str(DEFAULT_OFF_FORCEFIELD), default=DEFAULT_OFF_FORCEFIELD) parser.add_argument("-r", "--resolution", metavar="INT", type=int, - help="Rotamer library resolution in degrees. " - + "Default is " + str(DEFAULT_RESOLUTION), + help="Rotamer library resolution in degrees. " + + "Default is " + str(DEFAULT_RESOLUTION), default=DEFAULT_RESOLUTION) parser.add_argument("-o", "--output", metavar="PATH", - help="Output path. Default is the current working " - + "directory") + help="Output path. Default is the current working " + + "directory") parser.add_argument("--conformations_info_path", default=None, type=str, - help="Path to the folder containing the BCE output" - + " used to collect conformations for PELE") + help="Path to the folder containing the BCE output" + + " used to collect conformations for PELE") parser.add_argument('--with_solvent', dest='with_solvent', help="Generate solvent parameters for OBC", action='store_true') parser.add_argument('--as_datalocal', dest='as_datalocal', - help="Output will be saved following PELE's DataLocal " - + "hierarchy", action='store_true') + help="Output will be saved following PELE's " + + "DataLocal hierarchy", action='store_true') parser.add_argument('-c', '--charge_method', metavar="NAME", - type=str, help="The name of the method to use to " - + "compute charges", default=DEFAULT_CHARGE_METHOD, + type=str, help="The name of the method to use to " + + "compute charges", default=DEFAULT_CHARGE_METHOD, choices=AVAILABLE_CHARGE_METHODS) parser.add_argument('--charges_from_file', metavar="PATH", type=str, help="The path to the file with charges", @@ -77,8 +77,8 @@ def parse_args(args): parser.add_argument('--include_terminal_rotamers', dest="include_terminal_rotamers", action='store_true', - help="Not exclude terminal rotamers " - + "when building the rotamer library") + help="Not exclude terminal rotamers " + + "when building the rotamer library") parser.add_argument('-s', '--silent', dest="silent", action='store_true', @@ -87,12 +87,18 @@ def parse_args(args): dest="debug", action='store_true', help="Activate debug mode") + parser.add_argument('--for_amber', + dest="for_amber", + action='store_true', + help="Generate Impact template compatible with " + + "PELE\'s AMBER implementation") parser.set_defaults(as_datalocal=False) parser.set_defaults(with_solvent=False) parser.set_defaults(include_terminal_rotamers=False) parser.set_defaults(silent=False) parser.set_defaults(debug=False) + parser.set_defaults(for_amber=False) parsed_args = parser.parse_args(args) @@ -107,7 +113,8 @@ def run_peleffy(pdb_file, chain=None, exclude_terminal_rotamers=True, output=None, with_solvent=False, as_datalocal=False, - conformation_path=None): + conformation_path=None, + for_amber=False): """ It runs peleffy. @@ -139,8 +146,11 @@ def run_peleffy(pdb_file, not conformation_path: str Path to the BCE server outupt to use to extract dihedral angles - dihedral_mode: str - Select what kind of dihedrals to extract (all or only flexible) + for_amber : bool + Whether to generate an Impact template compatible with + PELE's AMBER implementation or not. Default is not, which + will create a template compatible with the standard OPLS2005 + force field of PELE """ if charges_from_file is not None: charge_method_str = 'file\n' \ @@ -161,6 +171,7 @@ def run_peleffy(pdb_file, log.info(' - Parameterization:') log.info(' - Force field:', forcefield_name) log.info(' - Charge method:', charge_method_str) + log.info(' - For AMBER:', for_amber) log.info(' - Rotamer library:') log.info(' - Resolution:', resolution) log.info(' - Exclude terminal rotamers:', exclude_terminal_rotamers) @@ -221,7 +232,7 @@ def run_peleffy(pdb_file, log.info(' - {} impropers'.format(len(topology.impropers))) # Generate the impact template - impact = Impact(topology) + impact = Impact(topology, for_amber=for_amber) impact.to_file(output_handler.get_impact_template_path()) # Generate the solvent template @@ -290,7 +301,8 @@ def main(args): as_datalocal=args.as_datalocal, chain=args.chain, conformation_path=args.conformations_info_path, - charges_from_file=args.charges_from_file) + charges_from_file=args.charges_from_file, + for_amber=args.for_amber) if __name__ == '__main__': diff --git a/peleffy/tests/test_main.py b/peleffy/tests/test_main.py index e8e43089..d19a50cb 100644 --- a/peleffy/tests/test_main.py +++ b/peleffy/tests/test_main.py @@ -83,6 +83,8 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.conformations_info_path is None, \ 'Unexpected conformations_path settings were parsed' + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' # Test custom shorts parsed_args = parse_args(['toluene.pdb', @@ -113,6 +115,8 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.conformations_info_path is None, \ 'Unexpected conformations_path settings were parsed' + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' # Test custom longs parsed_args = parse_args(['methane.pdb', @@ -144,6 +148,8 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.conformations_info_path is None, \ 'Unexpected conformations_path settings were parsed' + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' # Test unexpected charge method with pytest.raises(SystemExit) as pytest_wrapped_e: @@ -178,8 +184,10 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.conformations_info_path is None, \ 'Unexpected conformations_path settings were parsed' + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' - # Test charges_from_file argument + #  Test charges_from_file argument parsed_args = parse_args(['BHP.pdb', '--charges_from_file', 'BHP.mae']) @@ -207,6 +215,8 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.conformations_info_path is None, \ 'Unexpected conformations_path settings were parsed' + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' # Test include_terminal_rotamers argument parsed_args = parse_args(['methane.pdb', @@ -234,6 +244,8 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.conformations_info_path is None, \ 'Unexpected conformations_path settings were parsed' + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' # Test chain argument parsed_args = parse_args(['LYS_BNZ.pdb', @@ -261,6 +273,8 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.chain == 'L',\ 'Unexpected chain settings were parsed' + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' # Test silent argument parsed_args = parse_args(['methane.pdb', @@ -288,9 +302,8 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.conformations_info_path is None, \ 'Unexpected conformations_path settings were parsed' - - parse_args(['methane.pdb', '-s']) == parse_args(['methane.pdb', - '--silent']) + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' # Test debug argument parsed_args = parse_args(['methane.pdb', @@ -318,9 +331,8 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.conformations_info_path is None, \ 'Unexpected conformations_path settings were parsed' - - parse_args(['methane.pdb', '-d']) == parse_args(['methane.pdb', - '--debug']) + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' # Test dihedral library arguments parsed_args = parse_args(['methane.pdb', @@ -348,6 +360,37 @@ def test_peleffy_argparse(self): 'Unexpected with_solvent settings were parsed' assert parsed_args.conformations_info_path == "test_path", \ 'Unexpected conformations_path settings were parsed' + assert parsed_args.for_amber is False, \ + 'Unexpected for_amber settings were parsed' + + # Test for_amber argument + parsed_args = parse_args(['methane.pdb', + '--for_amber']) + + assert parsed_args.as_datalocal is False, \ + 'Unexpected as_datalocal settings were parsed' + assert parsed_args.charge_method == 'am1bcc', \ + 'Unexpected charge_method settings were parsed' + assert parsed_args.debug is False, \ + 'Unexpected debug settings were parsed' + assert parsed_args.forcefield == 'openff_unconstrained-1.3.0.offxml', \ + 'Unexpected forcefield settings were parsed' + assert parsed_args.include_terminal_rotamers is False, \ + 'Unexpected include_terminal_rotamers settings were parsed' + assert parsed_args.output is None, \ + 'Unexpected output settings were parsed' + assert parsed_args.pdb_file == 'methane.pdb', \ + 'Unexpected pdb_file settings were parsed' + assert parsed_args.resolution == 30, \ + 'Unexpected resolution settings were parsed' + assert parsed_args.silent is False, \ + 'Unexpected silent settings were parsed' + assert parsed_args.with_solvent is False, \ + 'Unexpected with_solvent settings were parsed' + assert parsed_args.conformations_info_path is None, \ + 'Unexpected conformations_path settings were parsed' + assert parsed_args.for_amber is True, \ + 'Unexpected for_amber settings were parsed' def test_peleffy_main(self): """It checks the main function of peleffy.""" From db59ef2294164aea202fea0155c5f3fd9dd24c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 29 Sep 2021 18:44:58 +0200 Subject: [PATCH 11/29] Extend utils tests --- peleffy/tests/test_utils.py | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/peleffy/tests/test_utils.py b/peleffy/tests/test_utils.py index def730cf..58bea71a 100644 --- a/peleffy/tests/test_utils.py +++ b/peleffy/tests/test_utils.py @@ -849,3 +849,57 @@ def test_raise_errors(self): with pytest.raises(ValueError): PDBreader = PDBFile(PATH_COMPLEX_PDB) _ = PDBreader.get_molecules_from_chain(selected_chain='A') + + def test_path_exists(self): + """ + It tests the method to check if a path exists. + """ + + from peleffy.utils import check_if_path_exists + from peleffy.utils import temporary_cd + + # Initialize temporary directory to check the method + with tempfile.TemporaryDirectory() as tmpdir: + with temporary_cd(tmpdir): + check_if_path_exists(tmpdir) + + # It should no longer work once the directory is removed + with pytest.raises(ValueError): + check_if_path_exists(tmpdir) + + def test_get_logger_level(self): + """ + It tests the level getter of peleffy's logger. + """ + from peleffy.utils import Logger + + logger = Logger() + + # Check default level + level = logger.get_level() + assert level == 'INFO', 'Invalid default Logger level' + + # Check debug level + logger.set_level('DEBUG') + level = logger.get_level() + assert level == 'DEBUG', 'Unexpected Logger level' + + # Check info level + logger.set_level('INFO') + level = logger.get_level() + assert level == 'INFO', 'Unexpected Logger level' + + # Check warning level + logger.set_level('WARNING') + level = logger.get_level() + assert level == 'WARNING', 'Unexpected Logger level' + + # Check error level + logger.set_level('ERROR') + level = logger.get_level() + assert level == 'ERROR', 'Unexpected Logger level' + + # Check critical level + logger.set_level('CRITICAL') + level = logger.get_level() + assert level == 'CRITICAL', 'Unexpected Logger level' From 8e90553a39fc884396bb21971f9482128d261416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 29 Sep 2021 19:06:32 +0200 Subject: [PATCH 12/29] Add docs and examples for new for_amber flag --- docs/usage.rst | 12 + .../AMBER-compatible_templates.ipynb | 187 ++++++++++++++++ .../AMBER-compatible_templates.ipynb | 207 ++++++++++++++++++ peleffy/tests/test_utils.py | 75 +++---- 4 files changed, 444 insertions(+), 37 deletions(-) create mode 100644 examples/OFF_parameterization/AMBER-compatible_templates.ipynb create mode 100644 examples/OPLS_parameterization/AMBER-compatible_templates.ipynb diff --git a/docs/usage.rst b/docs/usage.rst index 954944d6..4852c217 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -245,3 +245,15 @@ It always includes terminal rotamers, even if they belong to a terminal methyl g .. code-block:: bash $ python -m peleffy.main path/to/my_ligand.pdb --include_terminal_rotamers + +AMBER-compatible template +------------------------- +It generates an Impact template that will be compatible with PELE's AMBER force field implementation. The default template is compatible with OPLS's implementation. + +- Flag: ``--for_amber`` +- Default: ``False``, compatible with OPLS +- Example: the code below will generate an Impact template compatible with PELE'S AMBER + + .. code-block:: bash + + $ python -m peleffy.main path/to/my_ligand.pdb --for_amber \ No newline at end of file diff --git a/examples/OFF_parameterization/AMBER-compatible_templates.ipynb b/examples/OFF_parameterization/AMBER-compatible_templates.ipynb new file mode 100644 index 00000000..e025c755 --- /dev/null +++ b/examples/OFF_parameterization/AMBER-compatible_templates.ipynb @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "13879cec", + "metadata": {}, + "source": [ + "# Generation of an AMBER-compatible templates\n", + "This notebook shows how to use the API of `peleffy` to generate an Impact template that supports the AMBER force field implementation we have in PELE." + ] + }, + { + "cell_type": "markdown", + "id": "d22b7fdb", + "metadata": {}, + "source": [ + "### Load `peleffy`'s molecule representation with a PDB file of anthracene" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b380cbf2", + "metadata": {}, + "outputs": [], + "source": [ + "from peleffy.topology import Molecule" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9438c0b7", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " - Initializing molecule from a SMILES tag\n", + " - Loading molecule from RDKit\n", + " - Setting molecule name to 'c1ccc2cc3ccccc3cc2c1'\n", + " - Representing molecule with the Open Force Field Toolkit\n", + " - Generating rotamer library\n", + " - Core set to the center of the molecule\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "anthracene = Molecule(smiles='c1ccc2cc3ccccc3cc2c1', hydrogens_are_explicit=False)\n", + "display(anthracene)" + ] + }, + { + "cell_type": "markdown", + "id": "c154396b", + "metadata": {}, + "source": [ + "### Parameterize with `Open Force Field Toolkit` and generate the Impact template\n", + "Please, note that to save the Impact template in the required format for PELE's AMBER, we need to set `for_amber` to `True` when initializing the `Impact` class." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a817c5b9", + "metadata": {}, + "outputs": [], + "source": [ + "from peleffy.forcefield import OpenForceField\n", + "from peleffy.topology import Topology\n", + "from peleffy.template import Impact" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "71042711", + "metadata": {}, + "outputs": [], + "source": [ + "openff = OpenForceField('openff_unconstrained-2.0.0.offxml')\n", + "parameters = openff.parameterize(anthracene)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d228e024", + "metadata": {}, + "outputs": [], + "source": [ + "topology = Topology(anthracene, parameters)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7d3888da", + "metadata": {}, + "outputs": [], + "source": [ + "impact = Impact(topology, for_amber=True)\n", + "impact.to_file('antz')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/OPLS_parameterization/AMBER-compatible_templates.ipynb b/examples/OPLS_parameterization/AMBER-compatible_templates.ipynb new file mode 100644 index 00000000..fac325b9 --- /dev/null +++ b/examples/OPLS_parameterization/AMBER-compatible_templates.ipynb @@ -0,0 +1,207 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4a18bb3c", + "metadata": {}, + "source": [ + "# Generation of an AMBER-compatible templates\n", + "This notebook shows how to use the API of `peleffy` to generate an Impact template that supports the AMBER force field implementation we have in PELE." + ] + }, + { + "cell_type": "markdown", + "id": "3814435d", + "metadata": {}, + "source": [ + "### `peleffy` requires the Schrodinger Toolkit to use the OPLS2005 parameters\n", + "To indicate the path to the Schrodinger's installation `peleffy` needs the following environment variable to be set." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9439cebe", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "os.environ['SCHRODINGER'] = '/opt/schrodinger/suites2021-1/'" + ] + }, + { + "cell_type": "markdown", + "id": "99502cc9", + "metadata": {}, + "source": [ + "### Load `peleffy`'s molecule representation with a PDB file of anthracene" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6e0ab331", + "metadata": {}, + "outputs": [], + "source": [ + "from peleffy.topology import Molecule" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "828c3b82", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " - Initializing molecule from a SMILES tag\n", + " - Loading molecule from RDKit\n", + " - Setting molecule name to 'c1ccc2cc3ccccc3cc2c1'\n", + " - Representing molecule with the Open Force Field Toolkit\n", + " - Generating rotamer library\n", + " - Core set to the center of the molecule\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "anthracene = Molecule(smiles='c1ccc2cc3ccccc3cc2c1', hydrogens_are_explicit=False)\n", + "display(anthracene)" + ] + }, + { + "cell_type": "markdown", + "id": "21b43e83", + "metadata": {}, + "source": [ + "### Parameterize with `ffld_server`, and generate the Impact template\n", + "Please, note that to save the Impact template in the required format for PELE's AMBER, we need to set `for_amber` to `True` when initializing the `Impact` class." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a23e5108", + "metadata": {}, + "outputs": [], + "source": [ + "from peleffy.forcefield import OPLS2005ForceField\n", + "from peleffy.topology import Topology\n", + "from peleffy.template import Impact" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9356cf83", + "metadata": {}, + "outputs": [], + "source": [ + "opls_ff = OPLS2005ForceField()\n", + "parameters = opls_ff.parameterize(anthracene)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e3ec89c9", + "metadata": {}, + "outputs": [], + "source": [ + "topology = Topology(anthracene, parameters)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6f5aabe5", + "metadata": {}, + "outputs": [], + "source": [ + "impact = Impact(topology, for_amber=True)\n", + "impact.to_file('antz')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/peleffy/tests/test_utils.py b/peleffy/tests/test_utils.py index 58bea71a..0ebccc86 100644 --- a/peleffy/tests/test_utils.py +++ b/peleffy/tests/test_utils.py @@ -158,6 +158,39 @@ def push_messages(log): assert output == 'Critical message\n', \ 'Unexpected logger message at standard output' + def test_get_logger_level(self): + """ + It tests the level getter of peleffy's logger. + """ + from peleffy.utils import Logger + + logger = Logger() + + # Check debug level + logger.set_level('DEBUG') + level = logger.get_level() + assert level == 'DEBUG', 'Unexpected Logger level' + + # Check info level + logger.set_level('INFO') + level = logger.get_level() + assert level == 'INFO', 'Unexpected Logger level' + + # Check warning level + logger.set_level('WARNING') + level = logger.get_level() + assert level == 'WARNING', 'Unexpected Logger level' + + # Check error level + logger.set_level('ERROR') + level = logger.get_level() + assert level == 'ERROR', 'Unexpected Logger level' + + # Check critical level + logger.set_level('CRITICAL') + level = logger.get_level() + assert level == 'CRITICAL', 'Unexpected Logger level' + class TestOutputPathHandler(object): """ @@ -850,6 +883,11 @@ def test_raise_errors(self): PDBreader = PDBFile(PATH_COMPLEX_PDB) _ = PDBreader.get_molecules_from_chain(selected_chain='A') + +class TestGeneralUtils(object): + """ + It contains all the tests to validate general util methods. + """ def test_path_exists(self): """ It tests the method to check if a path exists. @@ -866,40 +904,3 @@ def test_path_exists(self): # It should no longer work once the directory is removed with pytest.raises(ValueError): check_if_path_exists(tmpdir) - - def test_get_logger_level(self): - """ - It tests the level getter of peleffy's logger. - """ - from peleffy.utils import Logger - - logger = Logger() - - # Check default level - level = logger.get_level() - assert level == 'INFO', 'Invalid default Logger level' - - # Check debug level - logger.set_level('DEBUG') - level = logger.get_level() - assert level == 'DEBUG', 'Unexpected Logger level' - - # Check info level - logger.set_level('INFO') - level = logger.get_level() - assert level == 'INFO', 'Unexpected Logger level' - - # Check warning level - logger.set_level('WARNING') - level = logger.get_level() - assert level == 'WARNING', 'Unexpected Logger level' - - # Check error level - logger.set_level('ERROR') - level = logger.get_level() - assert level == 'ERROR', 'Unexpected Logger level' - - # Check critical level - logger.set_level('CRITICAL') - level = logger.get_level() - assert level == 'CRITICAL', 'Unexpected Logger level' From 93a5d66f79b603d433d40e248526216270fc088f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 29 Sep 2021 19:11:55 +0200 Subject: [PATCH 13/29] Update releasehistory.rst --- docs/releasehistory.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/releasehistory.rst b/docs/releasehistory.rst index 3bd7720e..a2e4837b 100644 --- a/docs/releasehistory.rst +++ b/docs/releasehistory.rst @@ -8,6 +8,20 @@ Releases follow the ``major.minor.micro`` scheme recommended by `PEP440 `_: Adds support for PELE's AMBER with a new Impact template + +Tests added +""""""""""" +- `PR #155 `_: Extends the tests for utils module and introduces new tests for the new AMBER-compatible Impact template + + 1.3.4 - OpenFF-2.0 Support --------------------------------------------------------- From 72fc8cd7968313ce14391ed1ea95dddaed07cd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 29 Sep 2021 23:53:55 +0200 Subject: [PATCH 14/29] Fix bugs in PDB writer --- peleffy/data/tests/alchemical_structure.pdb | 33 +++++++++--------- peleffy/tests/test_alchemistry.py | 2 +- peleffy/utils/toolkits.py | 37 ++++++++++++--------- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/peleffy/data/tests/alchemical_structure.pdb b/peleffy/data/tests/alchemical_structure.pdb index 2444dbca..eabc19a2 100644 --- a/peleffy/data/tests/alchemical_structure.pdb +++ b/peleffy/data/tests/alchemical_structure.pdb @@ -15,30 +15,29 @@ HETATM 14 H6 HYB L 1 -1.369 1.481 -0.953 1.00 0.00 H HETATM 15 H7 HYB L 1 -1.903 0.672 1.034 1.00 0.00 H HETATM 16 H8 HYB L 1 -1.108 -0.781 1.667 1.00 0.00 H HETATM 17 H9 HYB L 1 -0.323 0.825 1.846 1.00 0.00 H -HETATM 18 H10 HYB L 1 1.237 0.709 -1.316 1.00 0.00 H -HETATM 19 H11 HYB L 1 0.622 1.801 -0.063 1.00 0.00 H -HETATM 20 C1 HYB L 1 -0.779 0.348 1.160 1.00 0.00 C -HETATM 21 C2 HYB L 1 -0.825 -0.520 2.218 1.00 0.00 C -HETATM 22 C3 HYB L 1 -0.996 -0.028 3.518 1.00 0.00 C -HETATM 23 C4 HYB L 1 -1.127 1.352 3.741 1.00 0.00 C -HETATM 24 C5 HYB L 1 -1.087 2.225 2.651 1.00 0.00 C -HETATM 25 C6 HYB L 1 -0.914 1.710 1.369 1.00 0.00 C -HETATM 26 H2 HYB L 1 -0.729 -1.582 2.050 1.00 0.00 H -HETATM 27 H3 HYB L 1 -1.027 -0.710 4.355 1.00 0.00 H -HETATM 28 H4 HYB L 1 -1.256 1.727 4.746 1.00 0.00 H -HETATM 29 H5 HYB L 1 -1.191 3.289 2.807 1.00 0.00 H -HETATM 30 H6 HYB L 1 -0.885 2.390 0.531 1.00 0.00 H +HETATM 18 H10 HYB L 1 1.237 0.709 -1.316 1.00 0.00 H +HETATM 19 H11 HYB L 1 0.622 1.801 -0.063 1.00 0.00 H +HETATM 20 C6 HYB L 1 -0.779 0.348 1.160 1.00 0.00 C +HETATM 21 C7 HYB L 1 -0.825 -0.520 2.218 1.00 0.00 C +HETATM 22 C8 HYB L 1 -0.996 -0.028 3.518 1.00 0.00 C +HETATM 23 C9 HYB L 1 -1.127 1.352 3.741 1.00 0.00 C +HETATM 24 C10 HYB L 1 -1.087 2.225 2.651 1.00 0.00 C +HETATM 25 C11 HYB L 1 -0.914 1.710 1.369 1.00 0.00 C +HETATM 26 H12 HYB L 1 -0.729 -1.582 2.050 1.00 0.00 H +HETATM 27 H13 HYB L 1 -1.027 -0.710 4.355 1.00 0.00 H +HETATM 28 H14 HYB L 1 -1.256 1.727 4.746 1.00 0.00 H +HETATM 29 H15 HYB L 1 -1.191 3.289 2.807 1.00 0.00 H +HETATM 30 H16 HYB L 1 -0.885 2.390 0.531 1.00 0.00 H CONECT 1 2 9 10 11 CONECT 2 3 4 5 20 CONECT 3 12 13 14 CONECT 4 15 16 17 CONECT 5 6 18 19 CONECT 6 7 7 8 -CONECT 20 21 21 25 +CONECT 20 21 25 CONECT 21 22 26 -CONECT 22 23 23 27 +CONECT 22 23 27 CONECT 23 24 28 -CONECT 24 25 25 29 +CONECT 24 25 29 CONECT 25 30 END - diff --git a/peleffy/tests/test_alchemistry.py b/peleffy/tests/test_alchemistry.py index af226fda..480f7b0d 100644 --- a/peleffy/tests/test_alchemistry.py +++ b/peleffy/tests/test_alchemistry.py @@ -3617,7 +3617,7 @@ def test_to_pdb(self): with tempfile.TemporaryDirectory() as tmpdir: with temporary_cd(tmpdir): - output_path = os.path.join(tmpdir, 'alchemical_structure.pdb') + output_path = os.path.join(tmpdir, 'hybrid.pdb') alchemizer.hybrid_to_pdb(output_path) compare_files(reference, output_path) diff --git a/peleffy/utils/toolkits.py b/peleffy/utils/toolkits.py index 14c85863..f3cbf386 100644 --- a/peleffy/utils/toolkits.py +++ b/peleffy/utils/toolkits.py @@ -299,7 +299,7 @@ def get_atom_names(self, molecule): for atom in rdkit_molecule.GetAtoms(): pdb_info = atom.GetPDBResidueInfo() - if pdb_info is not None: + if pdb_info is not None and pdb_info.GetName() != '': atom_names.append(pdb_info.GetName()) else: element = atom.GetSymbol() @@ -460,27 +460,28 @@ def to_pdb_file(self, molecule, path): Path to write to """ from rdkit import Chem + from copy import deepcopy assert Path(path).suffix == '.pdb', 'Wrong extension' - rdkit_molecule = molecule.rdkit_molecule + rdkit_molecule = deepcopy(molecule.rdkit_molecule) - pdb_block = Chem.rdmolfiles.MolToPDBBlock(rdkit_molecule) names = molecule.get_pdb_atom_names() tag = molecule.tag - renamed_pdb_block = '' - atom_counter = 0 - for line in pdb_block.split('\n'): - if line.startswith('HETATM'): - renamed_pdb_block += line[:12] + names[atom_counter] \ - + ' ' + tag + line[20:] + '\n' - atom_counter += 1 - else: - renamed_pdb_block += line + '\n' - - with open(path, 'w') as f: - f.write(renamed_pdb_block) + for idx, atom in enumerate(rdkit_molecule.GetAtoms()): + pdb_info = atom.GetPDBResidueInfo() + if pdb_info is None: + pdb_info = Chem.AtomPDBResidueInfo() + pdb_info.SetResidueNumber(1) + pdb_info.SetIsHeteroAtom(True) + pdb_info.SetResidueName(tag) + pdb_info.SetName(names[idx]) + if pdb_info.GetChainId() in (' ', ''): + pdb_info.SetChainId('L') + atom.SetPDBResidueInfo(pdb_info) + + Chem.rdmolfiles.MolToPDBFile(rdkit_molecule, path) def to_sdf_file(self, molecule, path): """ @@ -949,6 +950,12 @@ def alchemical_combination(self, mol1, mol2, atom_mapping, pdb_info.SetResidueNumber(1) pdb_info.SetChainId('L') pdb_info.SetIsHeteroAtom(True) + for atom in mol_combo.GetAtoms(): + atom.SetPDBResidueInfo(pdb_info) + + # Set all atoms as non-aromatic to avoid problems when kekulizing + for atom in mol_combo.GetAtoms(): + atom.SetIsAromatic(False) return mol_combo From 71dac1b0318a895a19cccec8bd275df6e004ac1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Thu, 30 Sep 2021 00:40:07 +0200 Subject: [PATCH 15/29] Fix bug with PDB atom names of alchemical structures --- peleffy/forcefield/parameters.py | 6 ++--- peleffy/topology/alchemistry.py | 46 ++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/peleffy/forcefield/parameters.py b/peleffy/forcefield/parameters.py index 9ab2b72b..9026b850 100644 --- a/peleffy/forcefield/parameters.py +++ b/peleffy/forcefield/parameters.py @@ -285,7 +285,7 @@ def correct_type(label, value): } # Skip data type transformation if the list is empty or None values - if value == [None,] * len(value): + if value == [None, ] * len(value): return value if value == []: return None @@ -296,8 +296,8 @@ def correct_type(label, value): correct_value = value if dict_units[label] is float: - correct_value = [float(v) if not v is None else v - for v in value] + correct_value = [float(v) if not v is None else v + for v in value] if dict_units[label] is simtk.unit.quantity.Quantity: correct_value = string_to_quantity(value) diff --git a/peleffy/topology/alchemistry.py b/peleffy/topology/alchemistry.py index ffa0d797..a6d1dd49 100644 --- a/peleffy/topology/alchemistry.py +++ b/peleffy/topology/alchemistry.py @@ -69,6 +69,9 @@ def __init__(self, topology1, topology2): # Generate alchemical graph self._graph, self._rotamers = self._generate_alchemical_graph() + # Assign PDB atom names + self._assign_pdb_atom_names() + @property def topology1(self): """ @@ -436,7 +439,6 @@ def _join_topologies(self): # General initializers logger = Logger() - rdkit_wrapper = RDKitToolkitWrapper() # First initialize the joint topology with topology 1 joint_topology = deepcopy(self.topology1) @@ -457,8 +459,6 @@ def _join_topologies(self): mol2_to_mol1_map = dict(zip(mol2_mapped_atoms, mol1_mapped_atoms)) # Add atoms from topology 2 that are missing in topology 1 - atom_names = self.molecule1.get_pdb_atom_names() - mol2_elements = rdkit_wrapper.get_elements(self.molecule2) mol2_to_alc_map = dict() for atom_idx, atom in enumerate(self.topology2.atoms): if atom_idx not in mol2_mapped_atoms: @@ -469,17 +469,6 @@ def _join_topologies(self): new_atom.set_index(new_index) mol2_to_alc_map[atom_idx] = new_index - # Handle name of new atom - counter = 1 - new_name = '{:^4}'.format(str(mol2_elements[atom_idx]) + - str(counter)) - while new_name in atom_names: - counter += 1 - new_name = '{:^4}'.format(str(mol2_elements[atom_idx]) + - str(counter)) - atom_names.append(new_name) - new_atom.set_PDB_name(new_name) - # Handle core allocation of new atom new_atom.set_as_branch() @@ -811,6 +800,35 @@ def _generate_alchemical_graph(self): return alchemical_graph, rotamers + def _assign_pdb_atom_names(self): + """ + It assigns consistent PDB atom names to the alchemical molecule. + """ + from peleffy.topology import Molecule + from peleffy.utils.toolkits import RDKitToolkitWrapper + + rdkit_wrapper = RDKitToolkitWrapper() + + # Combine molecules + mol_combo = \ + rdkit_wrapper.alchemical_combination(self.molecule1.rdkit_molecule, + self.molecule2.rdkit_molecule, + self.mapping, + self.connections) + + # Generate a dummy peleffy Molecule with the required information + # to extract PDB atom names + molecule = Molecule() + molecule._rdkit_molecule = mol_combo + molecule.set_tag('HYB') + + # Extract PDB atom names + atom_names = rdkit_wrapper.get_atom_names(molecule) + + # Assign PDB atom names + for atom_idx, atom in enumerate(self._joint_topology.atoms): + atom.set_PDB_name(atom_names[atom_idx].replace(' ', '_')) + def hybrid_to_pdb(self, path): """ Writes the alchemical molecule to a PDB file. From e68798e8528e9c594ce7be3a5aae8064ed84e447 Mon Sep 17 00:00:00 2001 From: laumalo Date: Fri, 8 Oct 2021 11:58:55 +0200 Subject: [PATCH 16/29] Fix bug and add checkers for --chain flag --- peleffy/main.py | 17 +++++++++++++--- peleffy/tests/test_main.py | 19 ++++++++++++++++++ peleffy/tests/test_utils.py | 40 +++++++++++++++++++++++++++++++++++++ peleffy/utils/input.py | 36 +++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 3 deletions(-) diff --git a/peleffy/main.py b/peleffy/main.py index 673adf76..4b7e642b 100644 --- a/peleffy/main.py +++ b/peleffy/main.py @@ -189,14 +189,25 @@ def run_peleffy(pdb_file, output = os.getcwd() # Initialize molecule + PDBreader = PDBFile(pdb_file) if chain is not None: - PDBreader = PDBFile(pdb_file) - molecule = PDBreader.get_molecules_from_chain( + molecules = PDBreader.get_molecules_from_chain( selected_chain=chain, rotamer_resolution=resolution, exclude_terminal_rotamers=exclude_terminal_rotamers) + + if PDBreader.is_unique(molecules): + molecule, = molecules + else: + raise ValueError('The selected chain must only contain a single ' + + 'molecule to be parameterized.') + else: - molecule = Molecule( + if PDBreader.is_complex: + raise ValueError('To parameterize a ligand from a protein-ligand ' + + 'complex PDB, a chain must be especified.') + else: + molecule = Molecule( pdb_file, rotamer_resolution=resolution, exclude_terminal_rotamers=exclude_terminal_rotamers) diff --git a/peleffy/tests/test_main.py b/peleffy/tests/test_main.py index d19a50cb..122b4f7e 100644 --- a/peleffy/tests/test_main.py +++ b/peleffy/tests/test_main.py @@ -425,3 +425,22 @@ def test_peleffy_main(self): logger = Logger() for handler in logger._logger.handlers: assert handler.level == logging.DEBUG + + def test_PDB_checks(self): + """ + It tests all the checks done when parsing a PDB to parameterize a molecule via the main module. + """ + from peleffy.main import run_peleffy + + # Protein-ligand PDB files requiere chain especification. + complex_path = get_data_file_path('complexes/LYS_BNZ.pdb') + with pytest.raises(ValueError): + _ = run_peleffy(complex_path, chain = None) + + # Multiple molecules on the same chain can not be parameterized + complex_path = get_data_file_path('complexes/complex_test.pdb') + with pytest.raises(ValueError): + _ = run_peleffy(complex_path, chain = 'C') + + + diff --git a/peleffy/tests/test_utils.py b/peleffy/tests/test_utils.py index 0ebccc86..20b01eb4 100644 --- a/peleffy/tests/test_utils.py +++ b/peleffy/tests/test_utils.py @@ -884,6 +884,46 @@ def test_raise_errors(self): _ = PDBreader.get_molecules_from_chain(selected_chain='A') + def test_is_complex(self): + """ + It tests the is_complex property of a PDBFile object. + """ + from peleffy.utils.input import PDBFile + from peleffy.utils import get_data_file_path + + # The PDB fetched is a complex + PATH_COMPLEX_PDB = get_data_file_path('complexes/LYS_BNZ.pdb') + PDBreader = PDBFile(PATH_COMPLEX_PDB) + assert(PDBreader.is_complex) + + # The PDB fetched is a ligand + PATH_LIGAND_PDB = get_data_file_path('ligands/benzene.pdb') + PDBreader = PDBFile(PATH_LIGAND_PDB) + assert not (PDBreader.is_complex) + + + def test_is_unique(self): + """ + It tests the static method is_unique. + """ + from peleffy.utils.input import PDBFile + from peleffy.utils import get_data_file_path + + PATH_COMPLEX_PDB = get_data_file_path('complexes/complex_test.pdb') + PDBreader = PDBFile(PATH_COMPLEX_PDB) + + # Chain with a single molecule + molecules = PDBreader.get_molecules_from_chain( + selected_chain='L', + allow_undefined_stereo = True) + assert(PDBreader.is_unique(molecules)) + + # Chain with multiple molecules + molecules = PDBreader.get_molecules_from_chain( + selected_chain='C', + allow_undefined_stereo = True) + assert not(PDBreader.is_unique(molecules)) + class TestGeneralUtils(object): """ It contains all the tests to validate general util methods. diff --git a/peleffy/utils/input.py b/peleffy/utils/input.py index c8404355..b8117289 100644 --- a/peleffy/utils/input.py +++ b/peleffy/utils/input.py @@ -234,3 +234,39 @@ def get_molecules_from_chain(self, selected_chain, rotamer_resolution=30, core_constraints=core_constraints) return molecules + + @property + def is_complex(self): + """ + Check whether the PDB fetched corresponds to a protein-ligand complex + or not. + + Returns + ------- + is_complex : bool + True if it is a protein-ligand complex. + """ + + if any(line.startswith('ATOM') for line in self.pdb_content) and \ + any(line.startswith('HETATM') for line in self.pdb_content): + return True + else: + return False + + @staticmethod + def is_unique(molecules): + """ + Check whether a list of molecules contains only one or multiple + elements. + + Parameters + ---------- + molecule : list[peleffy.topology.Molecule] + A list of peleffy's Molecule object. + + Returns + ------- + is_unique : bool + True if it only contains one molecule. + """ + return len(molecules) == 1 From c87fef1d337572c8c7792bd85e976d5dca1bd187 Mon Sep 17 00:00:00 2001 From: Laura Malo <44496034+laumalo@users.noreply.github.com> Date: Fri, 8 Oct 2021 13:11:02 +0200 Subject: [PATCH 17/29] Update releasehistory.rst --- docs/releasehistory.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/releasehistory.rst b/docs/releasehistory.rst index a2e4837b..fc4c16bf 100644 --- a/docs/releasehistory.rst +++ b/docs/releasehistory.rst @@ -17,10 +17,14 @@ New features """""""""""" - `PR #155 `_: Adds support for PELE's AMBER with a new Impact template +Bugfixes +"""""""" +- `PR #158 `_: Fix minor bug when using the --chain flag and introduces checks for the input PDB in the peleffy.main module. + Tests added """"""""""" - `PR #155 `_: Extends the tests for utils module and introduces new tests for the new AMBER-compatible Impact template - +- `PR #158 `_: Extends the tests for the new checks in the peleffy.main module. 1.3.4 - OpenFF-2.0 Support --------------------------------------------------------- From 92b37abb9b3c6319bd6bce86b7ebdfd466d3a95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Mon, 25 Oct 2021 20:49:07 +0200 Subject: [PATCH 18/29] Add support for alchemical OBC templates --- peleffy/data/tests/alchemical_mol1.pdb | 26 ++ peleffy/data/tests/alchemical_mol2.pdb | 28 ++ peleffy/solvent/solvent.py | 6 +- peleffy/tests/test_alchemistry.py | 377 ++++++++++++++++--------- peleffy/topology/alchemistry.py | 291 ++++++++++++------- peleffy/topology/topology.py | 8 +- 6 files changed, 485 insertions(+), 251 deletions(-) create mode 100644 peleffy/data/tests/alchemical_mol1.pdb create mode 100644 peleffy/data/tests/alchemical_mol2.pdb diff --git a/peleffy/data/tests/alchemical_mol1.pdb b/peleffy/data/tests/alchemical_mol1.pdb new file mode 100644 index 00000000..3f2b9392 --- /dev/null +++ b/peleffy/data/tests/alchemical_mol1.pdb @@ -0,0 +1,26 @@ +HETATM 1 C1 UNL L 1 -0.071 -1.459 -0.304 1.00 0.00 C +HETATM 2 N1 UNL L 1 -0.237 -0.057 -0.095 1.00 0.00 N1+ +HETATM 3 C2 UNL L 1 -1.150 0.412 -1.153 1.00 0.00 C +HETATM 4 C3 UNL L 1 -0.923 0.191 1.157 1.00 0.00 C +HETATM 5 C4 UNL L 1 0.942 0.732 -0.249 1.00 0.00 C +HETATM 6 C5 UNL L 1 2.046 0.408 0.633 1.00 0.00 C +HETATM 7 O1 UNL L 1 3.116 1.052 0.564 1.00 0.00 O +HETATM 8 O2 UNL L 1 1.991 -0.597 1.577 1.00 0.00 O1- +HETATM 9 H1 UNL L 1 -0.318 -1.760 -1.343 1.00 0.00 H +HETATM 10 H2 UNL L 1 -0.821 -1.989 0.347 1.00 0.00 H +HETATM 11 H3 UNL L 1 0.903 -1.865 -0.059 1.00 0.00 H +HETATM 12 H4 UNL L 1 -0.567 0.400 -2.104 1.00 0.00 H +HETATM 13 H5 UNL L 1 -2.067 -0.176 -1.186 1.00 0.00 H +HETATM 14 H6 UNL L 1 -1.369 1.481 -0.953 1.00 0.00 H +HETATM 15 H7 UNL L 1 -1.903 0.672 1.034 1.00 0.00 H +HETATM 16 H8 UNL L 1 -1.108 -0.781 1.667 1.00 0.00 H +HETATM 17 H9 UNL L 1 -0.323 0.825 1.846 1.00 0.00 H +HETATM 18 H10 UNL L 1 1.237 0.709 -1.316 1.00 0.00 H +HETATM 19 H11 UNL L 1 0.622 1.801 -0.063 1.00 0.00 H +CONECT 1 2 9 10 11 +CONECT 2 3 4 5 +CONECT 3 12 13 14 +CONECT 4 15 16 17 +CONECT 5 6 18 19 +CONECT 6 7 7 8 +END diff --git a/peleffy/data/tests/alchemical_mol2.pdb b/peleffy/data/tests/alchemical_mol2.pdb new file mode 100644 index 00000000..086b5db7 --- /dev/null +++ b/peleffy/data/tests/alchemical_mol2.pdb @@ -0,0 +1,28 @@ +COMPND HYDROLASE (SERINE PROTEINASE) +HETATM 1 C1 BEN A 1 -0.779 0.348 1.160 1.00 19.86 C +HETATM 2 C2 BEN A 1 -0.825 -0.520 2.218 1.00 19.86 C +HETATM 3 C3 BEN A 1 -0.996 -0.028 3.518 1.00 19.86 C +HETATM 4 C4 BEN A 1 -1.127 1.352 3.741 1.00 19.86 C +HETATM 5 C5 BEN A 1 -1.087 2.225 2.651 1.00 19.86 C +HETATM 6 C6 BEN A 1 -0.914 1.710 1.369 1.00 19.86 C +HETATM 7 C BEN A 1 -0.600 -0.141 -0.129 1.00 19.86 C +HETATM 8 N1 BEN A 1 -0.430 -1.431 -0.359 1.00 19.86 N +HETATM 9 N2 BEN A 1 -0.767 0.660 -1.167 1.00 19.86 N +HETATM 10 H2 BEN A 1 -0.729 -1.582 2.050 1.00 0.00 H +HETATM 11 H3 BEN A 1 -1.027 -0.710 4.355 1.00 0.00 H +HETATM 12 H4 BEN A 1 -1.256 1.727 4.746 1.00 0.00 H +HETATM 13 H5 BEN A 1 -1.191 3.289 2.807 1.00 0.00 H +HETATM 14 H6 BEN A 1 -0.885 2.390 0.531 1.00 0.00 H +HETATM 15 H1 1 BEN A 1 -0.302 -1.763 -1.304 1.00 0.00 H +HETATM 16 H1 2 BEN A 1 -0.429 -2.085 0.411 1.00 0.00 H +HETATM 17 HN2 BEN A 1 -0.637 0.307 -2.104 1.00 0.00 H +CONECT 1 2 2 6 7 +CONECT 2 3 10 +CONECT 3 4 4 11 +CONECT 4 5 12 +CONECT 5 6 6 13 +CONECT 6 14 +CONECT 7 8 9 9 +CONECT 8 15 16 +CONECT 9 17 +END diff --git a/peleffy/solvent/solvent.py b/peleffy/solvent/solvent.py index 3487d180..fb803a05 100644 --- a/peleffy/solvent/solvent.py +++ b/peleffy/solvent/solvent.py @@ -179,12 +179,10 @@ def to_dict(self): self._radii, self._scales): data['SolventParameters'][topology.molecule.tag] = dict() - atom_names = topology.molecule.get_pdb_atom_names() + atom_names = [atom.PDB_name for atom in topology.atoms] - for atom, name in zip(topology.molecule.rdkit_molecule.GetAtoms(), - atom_names): + for index, name in enumerate(atom_names): name = name.replace(' ', '_') - index = atom.GetIdx() data['SolventParameters'][topology.molecule.tag][name] = \ {'radius': round(radii[tuple((index, ))] .value_in_unit(unit.angstrom), 5), diff --git a/peleffy/tests/test_alchemistry.py b/peleffy/tests/test_alchemistry.py index 480f7b0d..12e3b76d 100644 --- a/peleffy/tests/test_alchemistry.py +++ b/peleffy/tests/test_alchemistry.py @@ -118,16 +118,16 @@ def test_alchemizer_initialization_checker(self): 'C=C', 'C(Cl)(Cl)(Cl)', [(0, 0), (1, 1), (2, 2), (3, 4)], - [6, ], - [5, ], + [6], + [5], [6, 7, 8], [], [], [4, 5], [3, 4], [0, 1, 5], - [], - [0, ] + [0, 1, 2, 3], + [0, 1] ), (None, None, @@ -139,13 +139,16 @@ def test_alchemizer_initialization_checker(self): [12, 13, 14], [12, 13, 14], [18, 19, 20, 21, 22, 23], + [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, + 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 50, 51, 52, 53], + [6, 7, 8, 9, 10, 11], [], [], [], - [], - [], - [], - [] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], + [0, 1, 2, 3, 4, 5] ), (None, None, @@ -157,13 +160,17 @@ def test_alchemizer_initialization_checker(self): [], [], [], - [], - [], + [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53], + [6, 7, 8, 9, 10, 11], [12, 13, 14], [12, 13, 14], [18, 19, 20, 21, 22, 23], - [], - [] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 28, 29], + [0, 1, 2, 3, 4, 5] ), ('ligands/acetylene.pdb', 'ligands/ethylene.pdb', @@ -173,12 +180,12 @@ def test_alchemizer_initialization_checker(self): [4, 5], [3, 4], [2, 3, 4, 5], - [1, 2], - [], - [], + [1, 2, 3, 4], + [0, 1], [], [], [], + [0], [] ), ('ligands/malonate.pdb', @@ -190,13 +197,17 @@ def test_alchemizer_initialization_checker(self): [10, ], [9, ], [13, 14, 15], - [23, 24], - [], - [], + [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44], + [2], [], [], [], - [] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22], + [0, 1] ), ('ligands/trimethylglycine.pdb', 'ligands/propionic_acid.pdb', @@ -208,14 +219,20 @@ def test_alchemizer_initialization_checker(self): [], [], [], - [], - [], + [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, + 66, 67], + [1], [7, 11, 12, 13, 14, 15, 16, 18], [7, 8, 9, 10, 11, 12, 15, 17], - [6, 7, 8, 9, 10, 11, 14, 19, 21, 22, 26, 27, - 28, 29, 30, 31, 32], - [40, 41], - [] + [6, 7, 8, 9, 10, 11, 14, 19, 21, 22, 26, + 27, 28, 29, 30, 31, 32], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45], + [0] ), ('ligands/trimethylglycine.pdb', 'ligands/benzamidine.pdb', @@ -230,17 +247,21 @@ def test_alchemizer_initialization_checker(self): 44, 45, 46, 47, 48, 49, 50, 51, 52], [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, - 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78], - [1, 2, 3, 4, 5, 6, 7], - [3, 4, 5, 6, 7, 10, 12, 13, 14, 15, 16, 17, 18], - [3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, + 79, 80, 81, 82, 83], + [1, 2, 3, 4, 5, 6, 7, 8], + [3, 4, 5, 6, 7, 10, 12, 13, 14, 15, 16, 17, + 18], + [3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17], [1, 2, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, - 28, 29, 30, 31, 32], - [3, 4, 5, 6, 7, 8, 9, 10, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, - 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 42, 43, 44, 45], + 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, + 27, 28, 29, 30, 31, 32], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45], [0] ) ]) @@ -335,11 +356,11 @@ def test_fep_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.2) @@ -369,11 +390,11 @@ def test_fep_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert (sigma2 / sigma1) - (1 - 0.2) < 1e-5, \ @@ -439,11 +460,11 @@ def test_fep_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.4) @@ -473,11 +494,11 @@ def test_fep_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert (sigma2 / sigma1) - 0.4 < 1e-5, \ @@ -742,11 +763,11 @@ def test_coul_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, coul_lambda=0.2) @@ -777,11 +798,11 @@ def test_coul_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert sigma2 - sigma1 < 1e-5, \ @@ -848,11 +869,11 @@ def test_coul_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, coul_lambda=0.5) @@ -883,11 +904,11 @@ def test_coul_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert sigma2 - sigma1 < 1e-5, \ @@ -1161,11 +1182,11 @@ def test_coul1_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, coul1_lambda=0.2) @@ -1196,11 +1217,11 @@ def test_coul1_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert sigma2 - sigma1 < 1e-5, \ @@ -1267,11 +1288,11 @@ def test_coul1_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, coul1_lambda=0.2) @@ -1302,11 +1323,11 @@ def test_coul1_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert sigma2 - sigma1 < 1e-5, \ @@ -1580,11 +1601,11 @@ def test_coul2_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, coul2_lambda=0.2) @@ -1615,11 +1636,11 @@ def test_coul2_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert sigma2 - sigma1 < 1e-5, \ @@ -1686,11 +1707,11 @@ def test_coul2_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, coul2_lambda=0.2) @@ -1721,11 +1742,11 @@ def test_coul2_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert sigma2 - sigma1 < 1e-5, \ @@ -1999,11 +2020,11 @@ def test_vdw_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, vdw_lambda=0.2) @@ -2034,11 +2055,11 @@ def test_vdw_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert (sigma2 / sigma1) - (1 - 0.2) < 1e-5, \ @@ -2105,11 +2126,11 @@ def test_vdw_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, vdw_lambda=0.2) @@ -2140,11 +2161,11 @@ def test_vdw_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert (sigma2 / sigma1) - 0.2 < 1e-5, \ @@ -2416,11 +2437,11 @@ def test_bonded_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, bonded_lambda=0.2) @@ -2451,11 +2472,11 @@ def test_bonded_lambda(self): for proper_idx in alchemizer._exclusive_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) - for improper_idx in alchemizer._exclusive_propers: + for improper_idx in alchemizer._exclusive_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert sigma2 - sigma1 < 1e-5, \ @@ -2522,11 +2543,11 @@ def test_bonded_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants1.append(proper.spring_constant) + proper_constants1.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants1.append(improper.spring_constant) + improper_constants1.append(improper.constant) top = alchemizer.get_alchemical_topology(fep_lambda=0.0, bonded_lambda=0.2) @@ -2557,11 +2578,11 @@ def test_bonded_lambda(self): for proper_idx in alchemizer._non_native_propers: proper = WritableProper(top.propers[proper_idx]) - proper_constants2.append(proper.spring_constant) + proper_constants2.append(proper.constant) for improper_idx in alchemizer._non_native_impropers: improper = WritableImproper(top.impropers[improper_idx]) - improper_constants2.append(improper.spring_constant) + improper_constants2.append(improper.constant) for sigma1, sigma2 in zip(sigmas1, sigmas2): assert sigma2 - sigma1 < 1e-5, \ @@ -3386,8 +3407,10 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, -0.0, -0.0], - [10.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, -0.0, -0.0], + [10.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0] ), (None, None, @@ -3399,22 +3422,20 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, None, None, None, - [1.256156174911, 1.256156174911, - 0.06697375586735, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, -0.0, 0.0, - 6.736762477654, 0.06697375586735, - 0.06697375586735, 0.0, 0.0, 0.0, + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.9974165607242, + 0.0, 0.0, 0.0, 0.0, 2.348375642009, + 2.348375642009, 0.9974165607242, 0.9974165607242, 3.661930099076, 3.661930099076, 3.661930099076, - 3.661930099076, 0.9974165607242, - 0.9974165607242, 6.736762477654, - 1.809047390003, 1.809047390003, - 3.661930099076,3.661930099076, + 3.661930099076, 6.736762477654, + 0.9974165607242, 0.9974165607242, + 6.736762477654, 1.809047390003, + 1.809047390003, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, @@ -3424,8 +3445,11 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, - -0.04816277792592, -0.04816277792592], - [0.0, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1] + 3.661930099076, 1.256156174911, + 1.256156174911, -0.04816277792592, + -0.04816277792592], + [0.0, 10.5, 1.0, 1.1, 1.1, 1.1, + 1.1, 1.1, 1.1] ), (None, None, @@ -3437,13 +3461,13 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, None, None, None, - [1.2319154369245875, 1.2319154369245875, - 0.06697375586735, 0.033486877933675, + [0.033486877933675, 0.033486877933675, + 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, -0.18516762066095, - 0.013324693850315, 3.401868116760675, - 0.06697375586735, 0.06697375586735, + 0.013324693850315, 0.033486877933675, + 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, 0.033486877933675, @@ -3460,12 +3484,13 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, 0.013324693850315, 0.013324693850315, 0.0744855238223, 0.0744855238223, 0.01480013640333, 0.01480013640333, + 1.1741878210045, 1.1741878210045, 0.4987082803621, 0.4987082803621, 1.830965049538, 1.830965049538, 1.830965049538, 1.830965049538, - 0.4987082803621, 0.4987082803621, - 3.368381238827, 0.9045236950015, - 0.9045236950015, 1.830965049538, + 3.368381238827, 0.4987082803621, + 0.4987082803621, 3.368381238827, + 0.9045236950015, 0.9045236950015, 1.830965049538, 1.830965049538, 1.830965049538, 1.830965049538, 1.830965049538, 1.830965049538, @@ -3475,10 +3500,11 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, 1.830965049538, 1.830965049538, 1.830965049538, 1.830965049538, 1.830965049538, 1.830965049538, - 1.830965049538, -0.02408138896296, - -0.02408138896296], - [5.25, 0.55, 0.55, 0.55, 0.55, 0.55, 0.55] - ), + 1.830965049538, 1.830965049538, + 0.6280780874555, 0.6280780874555, + -0.02408138896296, -0.02408138896296], + [5.25, 5.25, 0.5, 0.55, 0.55, 0.55, 0.55, + 0.55, 0.55] ), (None, None, 'C[N+](C)(C)CC(=O)[O-]', @@ -3516,8 +3542,10 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, -0.0, -0.0], - [10.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, -0.0, -0.0], + [10.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0] ), (None, None, @@ -3529,17 +3557,17 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, 0.0, 0.0, 1.0, - [1.256156174911, 1.256156174911, - 0.06697375586735, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, -0.0, 0.0, 6.736762477654, - 0.06697375586735, 0.06697375586735, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.9974165607242, 0.9974165607242, - 3.661930099076, 3.661930099076, + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 2.348375642009, + 2.348375642009, 0.9974165607242, + 0.9974165607242, 3.661930099076, 3.661930099076, 3.661930099076, + 3.661930099076, 6.736762477654, 0.9974165607242, 0.9974165607242, 6.736762477654, 1.809047390003, 1.809047390003, 3.661930099076, @@ -3552,9 +3580,11 @@ def test_angles_in_alchemical_topology(self, pdb1, pdb2, smiles1, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, 3.661930099076, - 3.661930099076, -0.04816277792592, + 3.661930099076, 1.256156174911, + 1.256156174911, -0.04816277792592, -0.04816277792592], - [0.0, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1] + [0.0, 10.5, 1.0, 1.1, 1.1, 1.1, 1.1, + 1.1, 1.1] ) ]) def test_dihedrals_in_alchemical_topology(self, pdb1, pdb2, smiles1, @@ -3597,7 +3627,7 @@ def test_dihedrals_in_alchemical_topology(self, pdb1, pdb2, smiles1, assert improper_constants == golden_improper_constants, \ 'Unexpected improper constants' - def test_to_pdb(self): + def test_hybrid_to_pdb(self): """ It validates the method to write the alchemical structure to a PDB file. @@ -3622,6 +3652,56 @@ def test_to_pdb(self): compare_files(reference, output_path) + def test_molecule1_to_pdb(self): + """ + It validates the method to write the first molecule structure + to a PDB file. + """ + import os + import tempfile + from peleffy.utils import get_data_file_path, temporary_cd + from peleffy.topology import Alchemizer + from peleffy.tests.utils import compare_files + + mol1, mol2, top1, top2 = generate_molecules_and_topologies_from_pdb( + 'ligands/trimethylglycine.pdb', 'ligands/benzamidine.pdb') + + reference = get_data_file_path('tests/alchemical_mol1.pdb') + + alchemizer = Alchemizer(top1, top2) + + with tempfile.TemporaryDirectory() as tmpdir: + with temporary_cd(tmpdir): + output_path = os.path.join(tmpdir, 'mol1.pdb') + alchemizer.molecule1_to_pdb(output_path) + + compare_files(reference, output_path) + + def test_molecule2_to_pdb(self): + """ + It validates the method to write the second molecule structure + to a PDB file. + """ + import os + import tempfile + from peleffy.utils import get_data_file_path, temporary_cd + from peleffy.topology import Alchemizer + from peleffy.tests.utils import compare_files + + mol1, mol2, top1, top2 = generate_molecules_and_topologies_from_pdb( + 'ligands/trimethylglycine.pdb', 'ligands/benzamidine.pdb') + + reference = get_data_file_path('tests/alchemical_mol2.pdb') + + alchemizer = Alchemizer(top1, top2) + + with tempfile.TemporaryDirectory() as tmpdir: + with temporary_cd(tmpdir): + output_path = os.path.join(tmpdir, 'mol2.pdb') + alchemizer.molecule2_to_pdb(output_path) + + compare_files(reference, output_path) + def test_rotamer_library_to_file(self): """ It validates the method to write the alchemical rotamer @@ -3644,3 +3724,30 @@ def test_rotamer_library_to_file(self): with temporary_cd(tmpdir): # TODO pass + + assert False + + def test_obc_parameters_to_file(self): + """ + It validates the method to write the OBC parameters + template. + """ + import os + import tempfile + from peleffy.utils import get_data_file_path, temporary_cd + from peleffy.topology import Alchemizer + from peleffy.tests.utils import compare_files + + mol1, mol2, top1, top2 = generate_molecules_and_topologies_from_pdb( + 'ligands/trimethylglycine.pdb', 'ligands/benzamidine.pdb') + + reference = get_data_file_path('tests/alchemical_structure.pdb') + + alchemizer = Alchemizer(top1, top2) + + with tempfile.TemporaryDirectory() as tmpdir: + with temporary_cd(tmpdir): + # TODO + pass + + assert False \ No newline at end of file diff --git a/peleffy/topology/alchemistry.py b/peleffy/topology/alchemistry.py index a6d1dd49..3851d912 100644 --- a/peleffy/topology/alchemistry.py +++ b/peleffy/topology/alchemistry.py @@ -337,6 +337,7 @@ def topology_from_lambda_set(self, lambda_set): reverse=False, final_state=mol2_angle) + # Joint topology cannot have mutual propers for proper_idx, proper in enumerate(alchemical_topology.propers): if proper_idx in self._exclusive_propers: proper.apply_lambda(["constant"], @@ -348,30 +349,7 @@ def topology_from_lambda_set(self, lambda_set): lambda_set.get_lambda_for_bonded(), reverse=True) - # TODO dihedrals cannot have mutual propers, all of them need to be non native or exclusive - atom1_idx = proper.atom1_idx - atom2_idx = proper.atom2_idx - atom3_idx = proper.atom3_idx - atom4_idx = proper.atom4_idx - if (atom1_idx in mol1_mapped_atoms and - atom2_idx in mol1_mapped_atoms and - atom3_idx in mol1_mapped_atoms and - atom4_idx in mol1_mapped_atoms): - mol_ids = (mol1_to_mol2_map[atom1_idx], - mol1_to_mol2_map[atom2_idx], - mol1_to_mol2_map[atom3_idx], - mol1_to_mol2_map[atom4_idx]) - - for mol2_proper in self.topology2.propers: - if (mol2_proper.atom1_idx in mol_ids and - mol2_proper.atom2_idx in mol_ids and - mol2_proper.atom3_idx in mol_ids and - mol2_proper.atom4_idx in mol_ids): - proper.apply_lambda(["constant"], - lambda_set.get_lambda_for_bonded(), - reverse=False, - final_state=mol2_proper) - + # Joint topology cannot have mutual impropers for improper_idx, improper in enumerate(alchemical_topology.impropers): if improper_idx in self._exclusive_impropers: improper.apply_lambda(["constant"], @@ -383,30 +361,6 @@ def topology_from_lambda_set(self, lambda_set): lambda_set.get_lambda_for_bonded(), reverse=True) - # TODO dihedrals cannot have mutual propers, all of them need to be non native or exclusive - atom1_idx = improper.atom1_idx - atom2_idx = improper.atom2_idx - atom3_idx = improper.atom3_idx - atom4_idx = improper.atom4_idx - if (atom1_idx in mol1_mapped_atoms and - atom2_idx in mol1_mapped_atoms and - atom3_idx in mol1_mapped_atoms and - atom4_idx in mol1_mapped_atoms): - mol_ids = (mol1_to_mol2_map[atom1_idx], - mol1_to_mol2_map[atom2_idx], - mol1_to_mol2_map[atom3_idx], - mol1_to_mol2_map[atom4_idx]) - - for mol2_improper in self.topology2.impropers: - if (mol2_improper.atom1_idx in mol_ids and - mol2_improper.atom2_idx in mol_ids and - mol2_improper.atom3_idx in mol_ids and - mol2_improper.atom4_idx in mol_ids): - improper.apply_lambda(["constant"], - lambda_set.get_lambda_for_bonded(), - reverse=False, - final_state=mol2_improper) - return alchemical_topology def _join_topologies(self): @@ -553,25 +507,22 @@ def _join_topologies(self): atom3_idx = proper.atom3_idx atom4_idx = proper.atom1_idx - if (atom1_idx not in mol2_mapped_atoms or - atom2_idx not in mol2_mapped_atoms or - atom3_idx not in mol2_mapped_atoms or - atom4_idx not in mol2_mapped_atoms): - new_proper = deepcopy(proper) + # Add a copy of the proper + new_proper = deepcopy(proper) - # Handle index of new atom - new_index = len(joint_topology.propers) - new_proper.set_index(new_index) + # Handle index of new atom + new_index = len(joint_topology.propers) + new_proper.set_index(new_index) - # Handle atom indices - new_proper.set_atom1_idx(mol2_to_alc_map[atom1_idx]) - new_proper.set_atom2_idx(mol2_to_alc_map[atom2_idx]) - new_proper.set_atom3_idx(mol2_to_alc_map[atom3_idx]) - new_proper.set_atom4_idx(mol2_to_alc_map[atom4_idx]) + # Handle atom indices + new_proper.set_atom1_idx(mol2_to_alc_map[atom1_idx]) + new_proper.set_atom2_idx(mol2_to_alc_map[atom2_idx]) + new_proper.set_atom3_idx(mol2_to_alc_map[atom3_idx]) + new_proper.set_atom4_idx(mol2_to_alc_map[atom4_idx]) - # Add new proper to the alchemical topology - non_native_propers.append(new_index) - joint_topology.add_proper(new_proper) + # Add new proper to the alchemical topology + non_native_propers.append(new_index) + joint_topology.add_proper(new_proper) # Add impropers for improper in self.topology2.impropers: @@ -580,25 +531,22 @@ def _join_topologies(self): atom3_idx = improper.atom3_idx atom4_idx = improper.atom1_idx - if (atom1_idx not in mol2_mapped_atoms or - atom2_idx not in mol2_mapped_atoms or - atom3_idx not in mol2_mapped_atoms or - atom4_idx not in mol2_mapped_atoms): - new_improper = deepcopy(improper) + # Add a copy of the improper + new_improper = deepcopy(improper) - # Handle index of new atom - new_index = len(joint_topology.impropers) - new_improper.set_index(new_index) + # Handle index of new atom + new_index = len(joint_topology.impropers) + new_improper.set_index(new_index) - # Handle atom indices - new_improper.set_atom1_idx(mol2_to_alc_map[atom1_idx]) - new_improper.set_atom2_idx(mol2_to_alc_map[atom2_idx]) - new_improper.set_atom3_idx(mol2_to_alc_map[atom3_idx]) - new_improper.set_atom4_idx(mol2_to_alc_map[atom4_idx]) + # Handle atom indices + new_improper.set_atom1_idx(mol2_to_alc_map[atom1_idx]) + new_improper.set_atom2_idx(mol2_to_alc_map[atom2_idx]) + new_improper.set_atom3_idx(mol2_to_alc_map[atom3_idx]) + new_improper.set_atom4_idx(mol2_to_alc_map[atom4_idx]) - # Add new improper to the alchemical topology - non_native_impropers.append(new_index) - joint_topology.add_improper(new_improper) + # Add new improper to the alchemical topology + non_native_impropers.append(new_index) + joint_topology.add_improper(new_improper) return joint_topology, non_native_atoms, non_native_bonds, \ non_native_angles, non_native_propers, non_native_impropers, \ @@ -660,29 +608,11 @@ def _get_exclusive_elements(self): # Identify propers for proper_idx, proper in enumerate(self.topology1.propers): - atom1_idx = proper.atom1_idx - atom2_idx = proper.atom2_idx - atom3_idx = proper.atom3_idx - atom4_idx = proper.atom3_idx - - if (atom1_idx not in mol1_mapped_atoms or - atom2_idx not in mol1_mapped_atoms or - atom3_idx not in mol1_mapped_atoms or - atom4_idx not in mol1_mapped_atoms): - exclusive_propers.append(proper_idx) + exclusive_propers.append(proper_idx) # Identify impropers for improper_idx, improper in enumerate(self.topology1.impropers): - atom1_idx = improper.atom1_idx - atom2_idx = improper.atom2_idx - atom3_idx = improper.atom3_idx - atom4_idx = improper.atom3_idx - - if (atom1_idx not in mol1_mapped_atoms or - atom2_idx not in mol1_mapped_atoms or - atom3_idx not in mol1_mapped_atoms or - atom4_idx not in mol1_mapped_atoms): - exclusive_impropers.append(improper_idx) + exclusive_impropers.append(improper_idx) return exclusive_atoms, exclusive_bonds, exclusive_angles, \ exclusive_propers, exclusive_impropers @@ -798,6 +728,31 @@ def _generate_alchemical_graph(self): else: atom.set_as_branch() + # Find absolute parent atom + absolute_parent = None + for atom in self._joint_topology.atoms: + if atom.core: + absolute_parent = atom.index + break + else: + logger.error(['Error: no core atom found in hybrid molecule']) + + # Get parent indexes from the molecular graph + parent_idxs = alchemical_graph.get_parents(absolute_parent) + + # Assert parent_idxs has right length + if len(parent_idxs) != len(self._joint_topology.atoms): + logger.error(['Error: invalid number of parents obtained for ' + + 'the hybrid molecule']) + + # Assign parent atoms + for atom in self._joint_topology.atoms: + parent_idx = parent_idxs[atom.index] + if parent_idx is not None: + atom.set_parent(self._joint_topology.atoms[parent_idx]) + else: + atom.set_parent(None) + return alchemical_graph, rotamers def _assign_pdb_atom_names(self): @@ -912,8 +867,10 @@ def rotamer_library_to_file(self, path, fep_lambda=None, of the rotamer libraries of both molecules, to the path that is supplied. - Returns - ------- + Parameters + ---------- + path : str + The path where to save the rotamer library fep_lambda : float The value to define an FEP lambda. This lambda affects all the parameters. It needs to be contained between @@ -945,11 +902,6 @@ def rotamer_library_to_file(self, path, fep_lambda=None, affects bonded parameters. It needs to be contained between 0 and 1. It has precedence over fep_lambda. Default is None - - Parameters - ---------- - path : str - The path where to save the rotamer library """ at_least_one = fep_lambda is not None or \ @@ -1013,13 +965,136 @@ def rotamer_library_to_file(self, path, fep_lambda=None, file.write(' sidelib FREE{} {} {} &\n'.format( rotamer.resolution, atom_name1, atom_name2)) + def obc_parameters_to_file(self, path, fep_lambda=None, + coul_lambda=None, coul1_lambda=None, + coul2_lambda=None, vdw_lambda=None, + bonded_lambda=None): + """ + It saves the alchemical OBC parameters, which is the combination + of the OBC parameters of both molecules, to the path that + is supplied. + + Parameters + ---------- + path : str + The path where to save the OBC parameters template + fep_lambda : float + The value to define an FEP lambda. This lambda affects + all the parameters. It needs to be contained between + 0 and 1. Default is None + coul_lambda : float + The value to define a general coulombic lambda. This lambda + only affects coulombic parameters of both molecules. It needs + to be contained between 0 and 1. It has precedence over + fep_lambda. Default is None + coul1_lambda : float + The value to define a coulombic lambda for exclusive atoms + of molecule 1. This lambda only affects coulombic parameters + of exclusive atoms of molecule 1. It needs to be contained + between 0 and 1. It has precedence over coul_lambda or + fep_lambda. Default is None + coul2_lambda : float + The value to define a coulombic lambda for exclusive atoms + of molecule 2. This lambda only affects coulombic parameters + of exclusive atoms of molecule 2. It needs to be contained + between 0 and 1. It has precedence over coul_lambda or + fep_lambda. Default is None + vdw_lambda : float + The value to define a vdw lambda. This lambda only + affects van der Waals parameters. It needs to be contained + between 0 and 1. It has precedence over fep_lambda. + Default is None + bonded_lambda : float + The value to define a coulombic lambda. This lambda only + affects bonded parameters. It needs to be contained + between 0 and 1. It has precedence over fep_lambda. + Default is None + + Returns + ------- + path : str + The path where to save the rotamer library + """ + + # Handle peleffy Logger + from peleffy.utils import Logger + + logger = Logger() + log_level = logger.get_level() + logger.set_level('CRITICAL') + + at_least_one = fep_lambda is not None or \ + coul_lambda is not None or coul1_lambda is not None or \ + coul2_lambda is not None or vdw_lambda is not None or \ + bonded_lambda is not None + + # Define lambdas + fep_lambda = FEPLambda(fep_lambda) + coul_lambda = CoulombicLambda(coul_lambda) + coul1_lambda = Coulombic1Lambda(coul1_lambda) + coul2_lambda = Coulombic2Lambda(coul2_lambda) + vdw_lambda = VanDerWaalsLambda(vdw_lambda) + bonded_lambda = BondedLambda(bonded_lambda) + + lambda_set = LambdaSet(fep_lambda, coul_lambda, coul1_lambda, + coul2_lambda, vdw_lambda, bonded_lambda) + + # Define mappers + mol1_mapped_atoms = [atom_pair[0] for atom_pair in self.mapping] + mol2_mapped_atoms = [atom_pair[1] for atom_pair in self.mapping] + mol1_to_mol2_map = dict(zip(mol1_mapped_atoms, mol2_mapped_atoms)) + + # Generate individual OBC parameters + from copy import deepcopy + from peleffy.solvent import OBC2 + + mol1_obc_params = OBC2(self.topology1) + mol2_obc_params = OBC2(self.topology2) + + # Generate alchemical OBC parameters object + alchemical_obc_params = deepcopy(mol1_obc_params) + alchemical_obc_params._topologies = [self._joint_topology, ] + + # Get OBC parameters of molecule 1 + radii1 = alchemical_obc_params._radii[0] + scales1 = alchemical_obc_params._scales[0] + + for atom_idx, atom in enumerate(self._joint_topology.atoms): + if atom_idx in self._exclusive_atoms: + lambda_value = 1.0 - lambda_set.get_lambda_for_coulomb1() + radius = radii1[(atom_idx, )] * lambda_value + scale = scales1[(atom_idx, )] * lambda_value + + elif atom_idx in self._non_native_atoms: + lambda_value = lambda_set.get_lambda_for_coulomb2() + radius = radii1[(atom_idx, )] * lambda_value + scale = scales1[(atom_idx, )] * lambda_value + + elif atom_idx in mol1_mapped_atoms: + mol2_idx = mol1_to_mol2_map[atom_idx] + radius2 = mol2_obc_params._radii[0][(mol2_idx, )] + scale2 = mol2_obc_params._scales[0][(mol2_idx, )] + + lambda_value = 1.0 - lambda_set.get_lambda_for_coulomb2() + radius = radii1[(atom_idx, )] * lambda_value \ + + (1.0 - lambda_value) * radius2 + scale = scales1[(atom_idx, )] * lambda_value \ + + (1.0 - lambda_value) * scale2 + + alchemical_obc_params._radii[0][(atom_idx, )] = radius + alchemical_obc_params._scales[0][(atom_idx, )] = scale + + alchemical_obc_params.to_file(path) + + logger.set_level(log_level) + def _ipython_display_(self): """ It returns a representation of the alchemical mapping. Returns ------- - mapping_representation : a IPython display object + mapping_representation : an IPython display object Displayable RDKit molecules with mapping information """ from IPython.display import display diff --git a/peleffy/topology/topology.py b/peleffy/topology/topology.py index 8c4c8708..b67311b9 100644 --- a/peleffy/topology/topology.py +++ b/peleffy/topology/topology.py @@ -144,16 +144,16 @@ def _build_atoms(self): absolute_parent = atom.index break else: - logger.error('Error: no core atom found in molecule ' - + '{}'.format(self.molecule.name)) + logger.error(['Error: no core atom found in molecule ' + + f'{self.molecule.name}']) # Get parent indexes from the molecular graph parent_idxs = self.molecule.graph.get_parents(absolute_parent) # Assert parent_idxs has right length if len(parent_idxs) != len(self.atoms): - logger.error('Error: no core atom found in molecule ' - + '{}'.format(self.molecule.name)) + logger.error(['Error: invalid number of parents obtained for ' + + f'{self.molecule.name}']) for atom in self.atoms: parent_idx = parent_idxs[atom.index] From 6fba5ec8673cda316d8efb6caea484920dd98992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Sun, 31 Oct 2021 18:20:02 +0100 Subject: [PATCH 19/29] Fix bug with non native cycles --- peleffy/topology/alchemistry.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/peleffy/topology/alchemistry.py b/peleffy/topology/alchemistry.py index 3851d912..f94a2a87 100644 --- a/peleffy/topology/alchemistry.py +++ b/peleffy/topology/alchemistry.py @@ -673,7 +673,14 @@ def _generate_alchemical_graph(self): index2 = mol2_to_mol1_map[mol2_edge[1]] # Get weights in each graph - weight1 = alchemical_graph[index1][index2]['weight'] + # Maybe the edge is not defined yet in the alchemical graph + if index2 not in alchemical_graph[index1]: + weight1 = 0 + alchemical_graph.add_edge(index1, index2, + weight=weight1) + else: + weight1 = alchemical_graph[index1][index2]['weight'] + weight2 = \ self.molecule2.graph[mol2_edge[0]][mol2_edge[1]]['weight'] @@ -1021,7 +1028,7 @@ def obc_parameters_to_file(self, path, fep_lambda=None, logger = Logger() log_level = logger.get_level() - logger.set_level('CRITICAL') + logger.set_level('ERROR') at_least_one = fep_lambda is not None or \ coul_lambda is not None or coul1_lambda is not None or \ @@ -1058,6 +1065,8 @@ def obc_parameters_to_file(self, path, fep_lambda=None, # Get OBC parameters of molecule 1 radii1 = alchemical_obc_params._radii[0] scales1 = alchemical_obc_params._scales[0] + radii2 = mol2_obc_params._radii[0] + scales2 = mol2_obc_params._scales[0] for atom_idx, atom in enumerate(self._joint_topology.atoms): if atom_idx in self._exclusive_atoms: @@ -1066,9 +1075,18 @@ def obc_parameters_to_file(self, path, fep_lambda=None, scale = scales1[(atom_idx, )] * lambda_value elif atom_idx in self._non_native_atoms: - lambda_value = lambda_set.get_lambda_for_coulomb2() - radius = radii1[(atom_idx, )] * lambda_value - scale = scales1[(atom_idx, )] * lambda_value + for mol2_index, alc_index in self._mol2_to_alc_map.items(): + if alc_index == atom_idx: + lambda_value = lambda_set.get_lambda_for_coulomb2() + radius = radii2[(mol2_index, )] * lambda_value + scale = scales2[(mol2_index, )] * lambda_value + break + else: + logger.error(['Error: mapping for atom index ' + + f'{atom_idx} not found in the ' + + 'hybrid molecule']) + radius = 0 + scale = 0 elif atom_idx in mol1_mapped_atoms: mol2_idx = mol1_to_mol2_map[atom_idx] From 303d0ddf4416ed59cca4b7499a42b293d8fd7914 Mon Sep 17 00:00:00 2001 From: Anna Gruszka Date: Fri, 10 Dec 2021 17:48:26 +0100 Subject: [PATCH 20/29] Bugfix: issues with long atom numbers and het extraction --- peleffy/utils/input.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/peleffy/utils/input.py b/peleffy/utils/input.py index c8404355..577f0d00 100644 --- a/peleffy/utils/input.py +++ b/peleffy/utils/input.py @@ -96,10 +96,18 @@ def _extract_molecules_from_chain(self, chain, rotamer_resolution, and line[22:26].strip() == residue_id] # Extract the PDB block of the molecule - pdb_block = [line for line in self.pdb_content - if (line.startswith('HETATM') or - line.startswith('CONECT')) - and any(' {} '.format(a) in line for a in atom_ids)] + pdb_block = [] + for line in self.pdb_content: + + if line.startswith('HETATM') and line[6:11].strip() in atom_ids: + pdb_block.append(line) + + if line.startswith('CONECT'): + stripped_line = line.replace("CONECT", "") + ids_in_line = [stripped_line[i:i + 5] for i in range(0, len(stripped_line), 5)] + + if any([atom_id in ids_in_line for atom_id in atom_ids]): + pdb_block.append(line) try: molecules.append( From e253f0d2eb3ec4d7666b711a5366c935a18ee9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 15 Dec 2021 12:43:44 +0100 Subject: [PATCH 21/29] Fix wrong link --- docs/releasehistory.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/releasehistory.rst b/docs/releasehistory.rst index fc4c16bf..4a1f303b 100644 --- a/docs/releasehistory.rst +++ b/docs/releasehistory.rst @@ -19,12 +19,13 @@ New features Bugfixes """""""" -- `PR #158 `_: Fix minor bug when using the --chain flag and introduces checks for the input PDB in the peleffy.main module. +- `PR #158 `_: Fix minor bug when using the --chain flag and introduces checks for the input PDB in the peleffy.main module. Tests added """"""""""" - `PR #155 `_: Extends the tests for utils module and introduces new tests for the new AMBER-compatible Impact template -- `PR #158 `_: Extends the tests for the new checks in the peleffy.main module. +- `PR #158 `_: Extends the tests for the new checks in the peleffy.main module. + 1.3.4 - OpenFF-2.0 Support --------------------------------------------------------- From 899569b31ebbf966e1623f5fd1dacf1bcafde697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 15 Dec 2021 12:48:04 +0100 Subject: [PATCH 22/29] Update releasehistory.rst --- docs/releasehistory.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/releasehistory.rst b/docs/releasehistory.rst index 4a1f303b..c11fb3fa 100644 --- a/docs/releasehistory.rst +++ b/docs/releasehistory.rst @@ -20,6 +20,7 @@ New features Bugfixes """""""" - `PR #158 `_: Fix minor bug when using the --chain flag and introduces checks for the input PDB in the peleffy.main module. +- `PR #159 `_: Fix issues with long atom numbers and heteromolecules extraction. Tests added """"""""""" From 45e15db9db72543814d6349bb0f80c9581102610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 15 Dec 2021 13:33:16 +0100 Subject: [PATCH 23/29] Add alchemistry example --- .../alchemistry/ethylene_to_chlorofom.ipynb | 489 ++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100644 examples/alchemistry/ethylene_to_chlorofom.ipynb diff --git a/examples/alchemistry/ethylene_to_chlorofom.ipynb b/examples/alchemistry/ethylene_to_chlorofom.ipynb new file mode 100644 index 00000000..f33fc034 --- /dev/null +++ b/examples/alchemistry/ethylene_to_chlorofom.ipynb @@ -0,0 +1,489 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "39a83a13", + "metadata": {}, + "source": [ + "# Alchemical transformation example\n", + "This example shows how to transform ethylene to chloroform using the Alchemistry module of peleffy." + ] + }, + { + "cell_type": "markdown", + "id": "3c3f4dfc", + "metadata": {}, + "source": [ + "## Initialize both molecules\n", + "The alchemical transformation requires the end states to be previously defined. We can initialize both molecules with peleffy with the standard `Molecule` representation. In this case, we define them using SMILES notation. However, we can also initialize them from PDB files." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ac61ec6b", + "metadata": {}, + "outputs": [], + "source": [ + "from peleffy.topology import Molecule" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c0edf576", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " - Initializing molecule from a SMILES tag\n", + " - Loading molecule from RDKit\n", + " - Representing molecule with the Open Force Field Toolkit\n", + " - Generating rotamer library\n", + " - Core set to the center of the molecule\n" + ] + } + ], + "source": [ + "ethylene = Molecule(smiles='C=C', hydrogens_are_explicit=False,\n", + " name='ethylene', tag='ETH')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "688db3ee", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " - Initializing molecule from a SMILES tag\n", + " - Loading molecule from RDKit\n", + " - Representing molecule with the Open Force Field Toolkit\n", + " - Generating rotamer library\n", + " - Core set to the center of the molecule\n" + ] + } + ], + "source": [ + "chloroform = Molecule(smiles='ClC(Cl)Cl', hydrogens_are_explicit=False,\n", + " name='chloroform', tag='CHL')" + ] + }, + { + "cell_type": "markdown", + "id": "07bce84e", + "metadata": {}, + "source": [ + "Once they are loaded, we can check if they were properly defined:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bb8ff83d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display(ethylene)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9e50d36e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display(chloroform)" + ] + }, + { + "cell_type": "markdown", + "id": "efd3c369", + "metadata": {}, + "source": [ + "## Parametrization and topology generation\n", + "The next step is to obtain the parameters and the topology for both molecules. This procedure can be done using peleffy's regular methods." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a768829e", + "metadata": {}, + "outputs": [], + "source": [ + "from peleffy.forcefield import OpenForceField\n", + "from peleffy.topology import Topology" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "156a1ee5", + "metadata": {}, + "outputs": [], + "source": [ + "openff = OpenForceField('openff_unconstrained-2.0.0.offxml')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3298309b", + "metadata": {}, + "outputs": [], + "source": [ + "ethylene_params = openff.parameterize(ethylene)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f01f410c", + "metadata": {}, + "outputs": [], + "source": [ + "chloroform_params = openff.parameterize(chloroform)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f9b98091", + "metadata": {}, + "outputs": [], + "source": [ + "ethylene_top = Topology(ethylene, ethylene_params)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "22d036e7", + "metadata": {}, + "outputs": [], + "source": [ + "chloroform_top = Topology(chloroform, chloroform_params)" + ] + }, + { + "cell_type": "markdown", + "id": "f4c62fb5", + "metadata": {}, + "source": [ + "## Alchemical transformation\n", + "Once we get the topology of both end states, we can initialize the `Alchemizer` module which will be able to generate the intermediate states according to the lambda values we supply." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a9f217ad", + "metadata": {}, + "outputs": [], + "source": [ + "from peleffy.topology import Alchemizer" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "0f48e95d", + "metadata": {}, + "outputs": [], + "source": [ + "alchemizer = Alchemizer(ethylene_top, chloroform_top)" + ] + }, + { + "cell_type": "markdown", + "id": "060e1d8a", + "metadata": {}, + "source": [ + "When `Alchemizer` is initialized, it tries to pair as many atom as possible between both end states, considering the Maximum Common Substructure (MCS). We can check this pairing:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "e3abc972", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display(alchemizer)" + ] + }, + { + "cell_type": "markdown", + "id": "6f4e03a3", + "metadata": {}, + "source": [ + "We can save end states and their hybrid structure to a PDB file. All PDB structures will be aligned according to the MCS to facilitate their comparison. The hybrid structure is the one that needs to be used in the PELE simulation since it contains all the required atoms to go from ethylene (state 1) to chloroform (state 2)." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "dfd5d180", + "metadata": {}, + "outputs": [], + "source": [ + "alchemizer.molecule1_to_pdb('state1.pdb')\n", + "alchemizer.molecule2_to_pdb('state2.pdb')\n", + "alchemizer.hybrid_to_pdb('hybrid.pdb')" + ] + }, + { + "cell_type": "markdown", + "id": "0d969206", + "metadata": {}, + "source": [ + "We can generate Impact templates with the `get_alchemical_topology()` method. It assigns the parameters to the hybrid structure according to the lambda value that we supply. Lambda values need to be contained between 0 and 1. A lambda of 0 represent state 1, a lambda of 1 represents state 2 and any value in between will be used to interpolate the parameters of each end state. There are different types of lambdas depending on the subset of parameters they modify:\n", + "- `fep_lambda`: it affects all the parameters.\n", + "- `coul_lambda`: it only affects coulombic parameters of both molecules. It has precedence over `fep_lambda`.\n", + "- `coul1_lambda`: it only affects coulombic parameters of exclusive atoms of molecule 1. It has precedence over `coul_lambda` and `fep_lambda`.\n", + "- `coul2_lambda`: it only affects coulombic parameters of exclusive atoms of molecule 2. It has precedence over `coul_lambda` and `fep_lambda`.\n", + "- `vdw_lambda`: it only affects van der Waals parameters. It has precedence over `fep_lambda`.\n", + "- `bonded_lambda`: it only affects bonded parameters. It has precedence over `fep_lambda`." + ] + }, + { + "cell_type": "markdown", + "id": "dca8fe33", + "metadata": {}, + "source": [ + "### Generate 9 alchemical states with `fep_lambda`" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d2755d38", + "metadata": {}, + "outputs": [], + "source": [ + "from peleffy.template import Impact" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "c122989b", + "metadata": {}, + "outputs": [], + "source": [ + "fep_lambdas = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "3de20abb", + "metadata": {}, + "outputs": [], + "source": [ + "for fep_lambda in fep_lambdas:\n", + " alchemical_top = alchemizer.get_alchemical_topology(fep_lambda=fep_lambda)\n", + " \n", + " template = Impact(alchemical_top)\n", + " template.to_file(f'hybz_{fep_lambda}')" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "1daf5d3d", + "metadata": {}, + "outputs": [], + "source": [ + "for fep_lambda in fep_lambdas:\n", + " alchemizer.rotamer_library_to_file(\n", + " f'HYB.rot.assign{fep_lambda}',\n", + " fep_lambda=fep_lambda)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "7119e7ef", + "metadata": {}, + "outputs": [], + "source": [ + "for fep_lambda in fep_lambdas:\n", + " alchemizer.obc_parameters_to_file(\n", + " f'ligandParams_{fep_lambda}.txt',\n", + " fep_lambda=fep_lambda)" + ] + }, + { + "cell_type": "markdown", + "id": "c0a3df45", + "metadata": {}, + "source": [ + "### Generate 14 alchemical states decoupling coulombic parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "8fedaec6", + "metadata": {}, + "outputs": [], + "source": [ + "fep_lambdas = [0.0, 0.0, 0.0, 0.0, 0.0,\n", + " 0.0, 0.2, 0.4, 0.6, 0.8,\n", + " 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]\n", + "coul1_lambdas = [0.0, 0.2, 0.4, 0.6, 0.8,\n", + " 1.0, 1.0, 1.0, 1.0, 1.0,\n", + " 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]\n", + "coul2_lambdas = [0.0, 0.0, 0.0, 0.0, 0.0,\n", + " 0.0, 0.0, 0.0, 0.0, 0.0,\n", + " 0.0, 0.2, 0.4, 0.6, 0.8, 1.0]" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "aa62b7ce", + "metadata": {}, + "outputs": [], + "source": [ + "for i, (fep_lambda, coul1_lambda, coul2_lambda) \\\n", + " in enumerate(zip(fep_lambdas, coul1_lambdas, coul2_lambdas)):\n", + " alchemical_top = alchemizer.get_alchemical_topology(\n", + " fep_lambda=fep_lambda,\n", + " coul1_lambda=coul1_lambda,\n", + " coul2_lambda=coul2_lambda)\n", + " \n", + " template = Impact(alchemical_top)\n", + " template.to_file(f'hybz_{i}')" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "619ed3b0", + "metadata": {}, + "outputs": [], + "source": [ + "for i, (fep_lambda, coul1_lambda, coul2_lambda) \\\n", + " in enumerate(zip(fep_lambdas, coul1_lambdas, coul2_lambdas)):\n", + " alchemizer.rotamer_library_to_file(\n", + " f'HYB.rot.assign{i}',\n", + " fep_lambda=fep_lambda,\n", + " coul1_lambda=coul1_lambda,\n", + " coul2_lambda=coul2_lambda)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "615e7dea", + "metadata": {}, + "outputs": [], + "source": [ + "for i, (fep_lambda, coul1_lambda, coul2_lambda) \\\n", + " in enumerate(zip(fep_lambdas, coul1_lambdas, coul2_lambdas)):\n", + " alchemizer.obc_parameters_to_file(\n", + " f'ligandParams_{i}.txt',\n", + " fep_lambda=fep_lambda,\n", + " coul1_lambda=coul1_lambda,\n", + " coul2_lambda=coul2_lambda)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 4d47c50f8d22277baadba10fbcd18928174024d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 15 Dec 2021 15:48:01 +0100 Subject: [PATCH 24/29] Fix alchemical test --- .../alchemistry/ethylene_to_chlorofom.ipynb | 4 +- peleffy/data/tests/alchemical_0.rot.assign | 4 + peleffy/data/tests/alchemical_1.rot.assign | 5 + peleffy/data/tests/alchemical_2.rot.assign | 2 + .../data/tests/alchemical_ligandParams_0.txt | 133 ++++++++++++++++++ .../data/tests/alchemical_ligandParams_1.txt | 133 ++++++++++++++++++ .../data/tests/alchemical_ligandParams_2.txt | 133 ++++++++++++++++++ peleffy/tests/test_alchemistry.py | 32 +++-- 8 files changed, 434 insertions(+), 12 deletions(-) create mode 100644 peleffy/data/tests/alchemical_0.rot.assign create mode 100644 peleffy/data/tests/alchemical_1.rot.assign create mode 100644 peleffy/data/tests/alchemical_2.rot.assign create mode 100644 peleffy/data/tests/alchemical_ligandParams_0.txt create mode 100644 peleffy/data/tests/alchemical_ligandParams_1.txt create mode 100644 peleffy/data/tests/alchemical_ligandParams_2.txt diff --git a/examples/alchemistry/ethylene_to_chlorofom.ipynb b/examples/alchemistry/ethylene_to_chlorofom.ipynb index f33fc034..2c8e967c 100644 --- a/examples/alchemistry/ethylene_to_chlorofom.ipynb +++ b/examples/alchemistry/ethylene_to_chlorofom.ipynb @@ -378,7 +378,7 @@ { "cell_type": "code", "execution_count": 20, - "id": "7119e7ef", + "id": "88c3359e", "metadata": {}, "outputs": [], "source": [ @@ -451,7 +451,7 @@ { "cell_type": "code", "execution_count": 24, - "id": "615e7dea", + "id": "f230a7cc", "metadata": {}, "outputs": [], "source": [ diff --git a/peleffy/data/tests/alchemical_0.rot.assign b/peleffy/data/tests/alchemical_0.rot.assign new file mode 100644 index 00000000..194d5c6f --- /dev/null +++ b/peleffy/data/tests/alchemical_0.rot.assign @@ -0,0 +1,4 @@ +rot assign res HYB & + sidelib FREE30 _N1_ _C4_ & + newgrp & + sidelib FREE30 _C5_ _C4_ & diff --git a/peleffy/data/tests/alchemical_1.rot.assign b/peleffy/data/tests/alchemical_1.rot.assign new file mode 100644 index 00000000..cefeefa6 --- /dev/null +++ b/peleffy/data/tests/alchemical_1.rot.assign @@ -0,0 +1,5 @@ +rot assign res HYB & + sidelib FREE30 _C4_ _N1_ & + sidelib FREE30 _C4_ _C5_ & + newgrp & + sidelib FREE30 _C6_ _N1_ & diff --git a/peleffy/data/tests/alchemical_2.rot.assign b/peleffy/data/tests/alchemical_2.rot.assign new file mode 100644 index 00000000..94f73e54 --- /dev/null +++ b/peleffy/data/tests/alchemical_2.rot.assign @@ -0,0 +1,2 @@ +rot assign res HYB & + sidelib FREE30 _N1_ _C6_ & diff --git a/peleffy/data/tests/alchemical_ligandParams_0.txt b/peleffy/data/tests/alchemical_ligandParams_0.txt new file mode 100644 index 00000000..044a23c0 --- /dev/null +++ b/peleffy/data/tests/alchemical_ligandParams_0.txt @@ -0,0 +1,133 @@ +{ + "SolventParameters": { + "Name": "OBC2", + "General": { + "solvent_dielectric": 78.5, + "solute_dielectric": 1, + "solvent_radius": 1.4, + "surface_area_penalty": 0.0054 + }, + "HYB": { + "_C1_": { + "radius": 1.7, + "scale": 0.72 + }, + "_N1_": { + "radius": 1.55, + "scale": 0.79 + }, + "_C2_": { + "radius": 1.7, + "scale": 0.72 + }, + "_C3_": { + "radius": 1.7, + "scale": 0.72 + }, + "_C4_": { + "radius": 1.7, + "scale": 0.72 + }, + "_C5_": { + "radius": 1.7, + "scale": 0.72 + }, + "_O1_": { + "radius": 1.5, + "scale": 0.85 + }, + "_O2_": { + "radius": 1.5, + "scale": 0.85 + }, + "_H1_": { + "radius": 1.2, + "scale": 0.85 + }, + "_H2_": { + "radius": 1.2, + "scale": 0.85 + }, + "_H3_": { + "radius": 1.2, + "scale": 0.85 + }, + "_H4_": { + "radius": 1.2, + "scale": 0.85 + }, + "_H5_": { + "radius": 1.2, + "scale": 0.85 + }, + "_H6_": { + "radius": 1.2, + "scale": 0.85 + }, + "_H7_": { + "radius": 1.2, + "scale": 0.85 + }, + "_H8_": { + "radius": 1.2, + "scale": 0.85 + }, + "_H9_": { + "radius": 1.2, + "scale": 0.85 + }, + "H10_": { + "radius": 1.2, + "scale": 0.85 + }, + "H11_": { + "radius": 1.2, + "scale": 0.85 + }, + "_C6_": { + "radius": 0.0, + "scale": 0.0 + }, + "_C7_": { + "radius": 0.0, + "scale": 0.0 + }, + "_C8_": { + "radius": 0.0, + "scale": 0.0 + }, + "_C9_": { + "radius": 0.0, + "scale": 0.0 + }, + "C10_": { + "radius": 0.0, + "scale": 0.0 + }, + "C11_": { + "radius": 0.0, + "scale": 0.0 + }, + "H12_": { + "radius": 0.0, + "scale": 0.0 + }, + "H13_": { + "radius": 0.0, + "scale": 0.0 + }, + "H14_": { + "radius": 0.0, + "scale": 0.0 + }, + "H15_": { + "radius": 0.0, + "scale": 0.0 + }, + "H16_": { + "radius": 0.0, + "scale": 0.0 + } + } + } +} \ No newline at end of file diff --git a/peleffy/data/tests/alchemical_ligandParams_1.txt b/peleffy/data/tests/alchemical_ligandParams_1.txt new file mode 100644 index 00000000..64554cdc --- /dev/null +++ b/peleffy/data/tests/alchemical_ligandParams_1.txt @@ -0,0 +1,133 @@ +{ + "SolventParameters": { + "Name": "OBC2", + "General": { + "solvent_dielectric": 78.5, + "solute_dielectric": 1, + "solvent_radius": 1.4, + "surface_area_penalty": 0.0054 + }, + "HYB": { + "_C1_": { + "radius": 1.625, + "scale": 0.755 + }, + "_N1_": { + "radius": 1.625, + "scale": 0.755 + }, + "_C2_": { + "radius": 1.625, + "scale": 0.755 + }, + "_C3_": { + "radius": 0.85, + "scale": 0.36 + }, + "_C4_": { + "radius": 0.85, + "scale": 0.36 + }, + "_C5_": { + "radius": 0.85, + "scale": 0.36 + }, + "_O1_": { + "radius": 0.75, + "scale": 0.425 + }, + "_O2_": { + "radius": 0.75, + "scale": 0.425 + }, + "_H1_": { + "radius": 1.25, + "scale": 0.85 + }, + "_H2_": { + "radius": 1.25, + "scale": 0.85 + }, + "_H3_": { + "radius": 0.6, + "scale": 0.425 + }, + "_H4_": { + "radius": 1.25, + "scale": 0.85 + }, + "_H5_": { + "radius": 0.6, + "scale": 0.425 + }, + "_H6_": { + "radius": 0.6, + "scale": 0.425 + }, + "_H7_": { + "radius": 0.6, + "scale": 0.425 + }, + "_H8_": { + "radius": 0.6, + "scale": 0.425 + }, + "_H9_": { + "radius": 0.6, + "scale": 0.425 + }, + "H10_": { + "radius": 0.6, + "scale": 0.425 + }, + "H11_": { + "radius": 0.6, + "scale": 0.425 + }, + "_C6_": { + "radius": 0.85, + "scale": 0.36 + }, + "_C7_": { + "radius": 0.85, + "scale": 0.36 + }, + "_C8_": { + "radius": 0.85, + "scale": 0.36 + }, + "_C9_": { + "radius": 0.85, + "scale": 0.36 + }, + "C10_": { + "radius": 0.85, + "scale": 0.36 + }, + "C11_": { + "radius": 0.85, + "scale": 0.36 + }, + "H12_": { + "radius": 0.6, + "scale": 0.425 + }, + "H13_": { + "radius": 0.6, + "scale": 0.425 + }, + "H14_": { + "radius": 0.6, + "scale": 0.425 + }, + "H15_": { + "radius": 0.6, + "scale": 0.425 + }, + "H16_": { + "radius": 0.6, + "scale": 0.425 + } + } + } +} \ No newline at end of file diff --git a/peleffy/data/tests/alchemical_ligandParams_2.txt b/peleffy/data/tests/alchemical_ligandParams_2.txt new file mode 100644 index 00000000..34ece285 --- /dev/null +++ b/peleffy/data/tests/alchemical_ligandParams_2.txt @@ -0,0 +1,133 @@ +{ + "SolventParameters": { + "Name": "OBC2", + "General": { + "solvent_dielectric": 78.5, + "solute_dielectric": 1, + "solvent_radius": 1.4, + "surface_area_penalty": 0.0054 + }, + "HYB": { + "_C1_": { + "radius": 1.55, + "scale": 0.79 + }, + "_N1_": { + "radius": 1.7, + "scale": 0.72 + }, + "_C2_": { + "radius": 1.55, + "scale": 0.79 + }, + "_C3_": { + "radius": 0.0, + "scale": 0.0 + }, + "_C4_": { + "radius": 0.0, + "scale": 0.0 + }, + "_C5_": { + "radius": 0.0, + "scale": 0.0 + }, + "_O1_": { + "radius": 0.0, + "scale": 0.0 + }, + "_O2_": { + "radius": 0.0, + "scale": 0.0 + }, + "_H1_": { + "radius": 1.3, + "scale": 0.85 + }, + "_H2_": { + "radius": 1.3, + "scale": 0.85 + }, + "_H3_": { + "radius": 0.0, + "scale": 0.0 + }, + "_H4_": { + "radius": 1.3, + "scale": 0.85 + }, + "_H5_": { + "radius": 0.0, + "scale": 0.0 + }, + "_H6_": { + "radius": 0.0, + "scale": 0.0 + }, + "_H7_": { + "radius": 0.0, + "scale": 0.0 + }, + "_H8_": { + "radius": 0.0, + "scale": 0.0 + }, + "_H9_": { + "radius": 0.0, + "scale": 0.0 + }, + "H10_": { + "radius": 0.0, + "scale": 0.0 + }, + "H11_": { + "radius": 0.0, + "scale": 0.0 + }, + "_C6_": { + "radius": 1.7, + "scale": 0.72 + }, + "_C7_": { + "radius": 1.7, + "scale": 0.72 + }, + "_C8_": { + "radius": 1.7, + "scale": 0.72 + }, + "_C9_": { + "radius": 1.7, + "scale": 0.72 + }, + "C10_": { + "radius": 1.7, + "scale": 0.72 + }, + "C11_": { + "radius": 1.7, + "scale": 0.72 + }, + "H12_": { + "radius": 1.2, + "scale": 0.85 + }, + "H13_": { + "radius": 1.2, + "scale": 0.85 + }, + "H14_": { + "radius": 1.2, + "scale": 0.85 + }, + "H15_": { + "radius": 1.2, + "scale": 0.85 + }, + "H16_": { + "radius": 1.2, + "scale": 0.85 + } + } + } +} \ No newline at end of file diff --git a/peleffy/tests/test_alchemistry.py b/peleffy/tests/test_alchemistry.py index 12e3b76d..747d7070 100644 --- a/peleffy/tests/test_alchemistry.py +++ b/peleffy/tests/test_alchemistry.py @@ -3702,7 +3702,12 @@ def test_molecule2_to_pdb(self): compare_files(reference, output_path) - def test_rotamer_library_to_file(self): + @pytest.mark.parametrize("fep_lambda, reference_path", + [(0.0, 'tests/alchemical_0.rot.assign'), + (0.5, 'tests/alchemical_1.rot.assign'), + (1.0, 'tests/alchemical_2.rot.assign') + ]) + def test_rotamer_library_to_file(self, fep_lambda, reference_path): """ It validates the method to write the alchemical rotamer library. @@ -3716,18 +3721,24 @@ def test_rotamer_library_to_file(self): mol1, mol2, top1, top2 = generate_molecules_and_topologies_from_pdb( 'ligands/trimethylglycine.pdb', 'ligands/benzamidine.pdb') - reference = get_data_file_path('tests/alchemical_structure.pdb') + reference = get_data_file_path(reference_path) alchemizer = Alchemizer(top1, top2) with tempfile.TemporaryDirectory() as tmpdir: with temporary_cd(tmpdir): - # TODO - pass + output_path = os.path.join(tmpdir, 'HYB.rot.assign') + alchemizer.rotamer_library_to_file(output_path, + fep_lambda=fep_lambda) - assert False + compare_files(reference, output_path) - def test_obc_parameters_to_file(self): + @pytest.mark.parametrize("fep_lambda, reference_path", + [(0.0, 'tests/alchemical_ligandParams_0.txt'), + (0.5, 'tests/alchemical_ligandParams_1.txt'), + (1.0, 'tests/alchemical_ligandParams_2.txt') + ]) + def test_obc_parameters_to_file(self, fep_lambda, reference_path): """ It validates the method to write the OBC parameters template. @@ -3741,13 +3752,14 @@ def test_obc_parameters_to_file(self): mol1, mol2, top1, top2 = generate_molecules_and_topologies_from_pdb( 'ligands/trimethylglycine.pdb', 'ligands/benzamidine.pdb') - reference = get_data_file_path('tests/alchemical_structure.pdb') + reference = get_data_file_path(reference_path) alchemizer = Alchemizer(top1, top2) with tempfile.TemporaryDirectory() as tmpdir: with temporary_cd(tmpdir): - # TODO - pass + output_path = os.path.join(tmpdir, 'ligandParams.txt') + alchemizer.obc_parameters_to_file(output_path, + fep_lambda=fep_lambda) - assert False \ No newline at end of file + compare_files(reference, output_path) \ No newline at end of file From d1c5a4f4cf447215b7f9ef9ddd016002c7a9405f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 15 Dec 2021 15:48:55 +0100 Subject: [PATCH 25/29] Ignore .DS_Store --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 76739042..9cb81f3e 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ dmypy.json # Pyre type checker .pyre/ + +# MacOS +.DS_Store From 75cc85ab8d48da3808dfd2f78cda63d6523a660e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 15 Dec 2021 16:00:54 +0100 Subject: [PATCH 26/29] Small code corrections --- peleffy/topology/alchemistry.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/peleffy/topology/alchemistry.py b/peleffy/topology/alchemistry.py index f94a2a87..b82a7038 100644 --- a/peleffy/topology/alchemistry.py +++ b/peleffy/topology/alchemistry.py @@ -1030,11 +1030,6 @@ def obc_parameters_to_file(self, path, fep_lambda=None, log_level = logger.get_level() logger.set_level('ERROR') - at_least_one = fep_lambda is not None or \ - coul_lambda is not None or coul1_lambda is not None or \ - coul2_lambda is not None or vdw_lambda is not None or \ - bonded_lambda is not None - # Define lambdas fep_lambda = FEPLambda(fep_lambda) coul_lambda = CoulombicLambda(coul_lambda) @@ -1149,7 +1144,6 @@ def __init__(self, value=None): "It has to be between 0 and 1") self._value = value - pass @property def value(self): From 003a153340ee0a419fcc4f01f5da90d0ce2d3a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 15 Dec 2021 16:03:05 +0100 Subject: [PATCH 27/29] Fix missing logger --- peleffy/topology/alchemistry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/peleffy/topology/alchemistry.py b/peleffy/topology/alchemistry.py index b82a7038..32aef9c7 100644 --- a/peleffy/topology/alchemistry.py +++ b/peleffy/topology/alchemistry.py @@ -656,6 +656,11 @@ def _generate_alchemical_graph(self): """ from copy import deepcopy + # Handle peleffy Logger + from peleffy.utils import Logger + + logger = Logger() + # Define mappers mol1_mapped_atoms = [atom_pair[0] for atom_pair in self.mapping] mol2_mapped_atoms = [atom_pair[1] for atom_pair in self.mapping] From 8ab7ff92442bc5a8c161c200b83cb2b68c759432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 15 Dec 2021 16:37:05 +0100 Subject: [PATCH 28/29] Upgrade openff-toolkit --- devtools/conda/meta.yaml | 2 +- devtools/envs/standard.yaml | 2 +- setup.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/devtools/conda/meta.yaml b/devtools/conda/meta.yaml index 09d796f7..0c914db6 100644 --- a/devtools/conda/meta.yaml +++ b/devtools/conda/meta.yaml @@ -24,7 +24,7 @@ requirements: - networkx - rdkit - ambertools - - openff-toolkit==0.10.0 + - openff-toolkit==0.10.1 about: home: https://github.com/martimunicoy/peleffy diff --git a/devtools/envs/standard.yaml b/devtools/envs/standard.yaml index b2dfbe61..9b8f465a 100644 --- a/devtools/envs/standard.yaml +++ b/devtools/envs/standard.yaml @@ -13,4 +13,4 @@ dependencies: - ipython - ambertools - rdkit - - openff-toolkit==0.10.0 + - openff-toolkit==0.10.1 diff --git a/setup.py b/setup.py index 553bd540..5948259d 100644 --- a/setup.py +++ b/setup.py @@ -34,14 +34,15 @@ def find_package_data(data_root, package_root): + 'a Python package that builds PELE-compatible force ' + 'field templates.', classifiers=[ - "Development Status :: 1 - Planning", + "Development Status :: 5 - Production/Stable", "Natural Language :: English", "Environment :: Console", "Intended Audience :: Science/Research", "Topic :: Utilities", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Operating System :: Unix" + "Operating System :: Unix", + "Operating System :: MacOS" ], version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), From acd12a31dae24272e91e5d6a3974139fb2aa8321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Wed, 15 Dec 2021 16:41:35 +0100 Subject: [PATCH 29/29] Update releasehistory.rst --- docs/releasehistory.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/releasehistory.rst b/docs/releasehistory.rst index c11fb3fa..bf49771a 100644 --- a/docs/releasehistory.rst +++ b/docs/releasehistory.rst @@ -15,17 +15,20 @@ This is a minor release of peleffy that adds a new module to generate alchemical New features """""""""""" +- `PR #154 `_: Introduces Alchemizer module to generate hybrid topologies - `PR #155 `_: Adds support for PELE's AMBER with a new Impact template +- `PR #162 `_: Upgrade to openff-toolkit 0.10.1 Bugfixes """""""" -- `PR #158 `_: Fix minor bug when using the --chain flag and introduces checks for the input PDB in the peleffy.main module. -- `PR #159 `_: Fix issues with long atom numbers and heteromolecules extraction. +- `PR #158 `_: Fix minor bug when using the --chain flag and introduces checks for the input PDB in the peleffy.main module +- `PR #159 `_: Fix issues with long atom numbers and heteromolecules extraction Tests added """"""""""" +- `PR #154 `_: Adds a collection of tests for Alchemizer module - `PR #155 `_: Extends the tests for utils module and introduces new tests for the new AMBER-compatible Impact template -- `PR #158 `_: Extends the tests for the new checks in the peleffy.main module. +- `PR #158 `_: Extends the tests for the new checks in the peleffy.main module 1.3.4 - OpenFF-2.0 Support