Source code for schrodinger.application.desmond.fep_scholar_util

"""
This file contains common utility methods and classes used in several FEP
scripts.

Copyright Schrodinger, LLC. All rights reserved.
"""

import future.utils
import math
import os

from schrodinger.application.desmond import constants
from schrodinger.application.desmond import fep_scholar_rules as rules
from schrodinger.application.desmond import r_group_asl
from schrodinger.application.desmond.fep_struc import SchrodStruc
from schrodinger.infra import canvas2d
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.structutils import rmsd
from schrodinger.ui.qt import structure2d
from schrodinger.utils import subprocess

# distance cutoff used for atom mapping
DISTANCE_CUTOFF = 0.40
# Decrement function, which is used to convert mmct indices to chmol ones
DECREMENT = lambda x: x - 1
# Color used to highlight mutation atoms.
MUTATION_COLOR = QtGui.QColor(255, 0, 0)
# Color used to highlight MCS core.
MCS_COLOR = QtGui.QColor(0, 255, 0)
# Color used to highlight MCS core.
CORE_COLOR = QtGui.QColor(0, 255, 0)
# Color used to highlight 'hot atoms'.
HOT_ATOMS_COLOR = QtGui.QColor(255, 140, 0)
# Constants used to apply template
IGNORE_HYDROGEN = 0
RESCALE = 2
FIXUP = True


[docs]def get_chmmol_bonds_from_atoms(chmmol, atoms): """ This function returns a list of bonds that connect atoms in a given list. :param chmmol: molecule structure :type chmmol: `canvas.ChmMol` :param atoms: list of atom indices :type atoms: list """ atoms = set(atoms) core_bonds = [] swigchmmol = canvas2d.convertChmMoltoSWIG(chmmol) bonds = swigchmmol.getBonds(True) for i, bond in enumerate(bonds): a1, a2 = bond.atom1(), bond.atom2() if a1.getMolIndex() in atoms and a2.getMolIndex() in atoms: core_bonds.append(i) return core_bonds
[docs]def get_mutation_atoms_and_bonds(cmol, match, mut_atoms): """ This function determines lists of atoms and bonds that should be highlighted to show mutations. :param cmol: Canvas ChmMol :type cmol: `canvas2d.ChmMol` :param match: list of core atoms :type match: list :param mut_atoms: list of 'mutated' atoms :type mut_atoms: list :return: tuple that contains list of atoms and list of bonds :rtype: tuple """ cmatch = list(map(DECREMENT, match)) cmut = list(map(DECREMENT, mut_atoms)) allatoms = set(range(cmol.getAtomCount())) atoms = list(allatoms - (set(cmatch) - set(cmut))) corebonds = get_chmmol_bonds_from_atoms(cmol, cmatch) allbonds = set(range(cmol.getBondCount())) bonds = list(allbonds.difference(corebonds)) return atoms, bonds
[docs]def generate_default_pic(ligand1, ligand2): """ Create default picture of two ligands, which only shows ligand structures and no mapping. :param ligand1: first ligand FEP structure object :type ligand1: `fep_scholar_util.FEPStructureObject` :param ligand2: second ligand FEP structure object :type ligand2: `fep_scholar_util.FEPStructureObject` """ for item in [ligand1.item, ligand2.item]: item.generate_picture(gen_coord=False)
[docs]def generate_mutation_pic(mapper, ligand1, ligand2): """ Create mutations on ligand structures. :param mapper: FEP scholar mapper object :type mapper: `fep_scholar_util.FEPScholarMapper` :param ligand1: first ligand FEP structure object :type ligand1: `fep_scholar_util.FEPStructureObject` :param ligand2: second ligand FEP structure object :type ligand2: `fep_scholar_util.FEPStructureObject` """ for item, matches, mut_atoms in zip([ligand1.item, ligand2.item], mapper.getMatches(), mapper.getMutations()): atoms, bonds = get_mutation_atoms_and_bonds(item.chmmol, matches, mut_atoms) item.generatePictureHighlight(atoms, bonds, MUTATION_COLOR, False)
[docs]def generate_mapping_pic(mapper, ligand1, ligand2): """ Create atom mapping by displaying atom indices on ligand structures. For the second ligand we display corresponding atom indices from the first ligand. :param mapper: FEP scholar mapper object :type mapper: `fep_scholar_util.FEPScholarMapper` :param ligand1: first ligand FEP structure object :type ligand1: `fep_scholar_util.FEPStructureObject` :param ligand2: second ligand FEP structure object :type ligand2: `fep_scholar_util.FEPStructureObject` """ match1, match2 = mapper.getMatches() # decrement atom indices in matches because ChmMol is 0-based, while # mmct is 1-based. matches1 = list(map(DECREMENT, match1)) matches2 = list(map(DECREMENT, match2)) atom_annotator = MappingAnnotator(matches1) atom_annotator2 = MappingAnnotator(matches2, matches1) ligand1.item.add_annotator(atom_annotator) ligand1.item.generate_picture(gen_coord=False) ligand2.item.add_annotator(atom_annotator2) ligand2.item.generate_picture(gen_coord=False)
[docs]def generate_core_pic(mapper, ligand1, ligand2): """ Highlight core atoms and bonds in ligand structures. :param mapper: FEP scholar mapper object :type mapper: `fep_scholar_util.FEPScholarMapper` :param ligand1: first ligand FEP structure object :type ligand1: `fep_scholar_util.FEPStructureObject` :param ligand2: second ligand FEP structure object :type ligand2: `fep_scholar_util.FEPStructureObject` """ match1, match2 = mapper.getMatches() for item, matches in zip([ligand1.item, ligand2.item], [match1, match2]): atoms = list(map(DECREMENT, matches)) bonds = get_chmmol_bonds_from_atoms(item.chmmol, atoms) item.generatePictureHighlight(atoms, bonds, CORE_COLOR, False)
[docs]def generate_hot_atoms_pic(mapper, ligand1, ligand2): """ Show 'hot' atoms on ligand structures. :param mapper: FEP scholar mapper object :type mapper: `fep_scholar_util.FEPScholarMapper` :param ligand1: first ligand FEP structure object :type ligand1: `fep_scholar_util.FEPStructureObject` :param ligand2: second ligand FEP structure object :type ligand2: `fep_scholar_util.FEPStructureObject` """ for item, hot_atoms in zip([ligand1.item, ligand2.item], mapper.getHotAtoms()): atoms = list(map(DECREMENT, hot_atoms)) bonds = get_chmmol_bonds_from_atoms(item.chmmol, atoms) item.generatePictureHighlight(atoms, bonds, HOT_ATOMS_COLOR, False)
[docs]class MappingAnnotator(canvas2d.ChemViewAnnotator): """ This annotator allows to show atom numbers for a selected subset of atoms. In addition user can specify 'map to' list of atom numbers that will be used instead to as atom labels. """
[docs] def __init__(self, matches, map_to=None): """ Instantiate a MappingAnnotator instance. :param matches: list of atoms for which atom labels will be shown. :type matches: list :param map_to: list of atom numbers that will be used as atom labels. Atom indices in this list are zero-based. :type map_to: list """ canvas2d.ChemViewAnnotator.__init__(self) self.matches = matches self.map_to = map_to
[docs] def annotate(self, view, dm): """ Add atom number to each atom :type view: Chemview :param view: the View this item goes in :type dm: ChmDrawMol :param dm: object that contains the list of atom and bond graphics """ for i, a in enumerate(dm.atoms): if i in self.matches: if self.map_to is None: a.replaceLabel(str(i + 1), "") else: idx = self.matches.index(i) a.replaceLabel(str(self.map_to[idx] + 1), "") dm.recreateBonds()
[docs]class MetaAtom(object): """ This is the meta atom class that mostly provides convinience functions. It is used by the scholar mapper functions. """
[docs] def __init__(self, atom): self._atom = atom
@property def index(self): return self._atom.index @property def atom_type(self): """ Return macromodel atom type. """ return self._atom.atom_type @property def xyz(self): return self._atom.xyz @property def connected_atoms(self): """ Return a list of atoms that are bonded to this atom. """ return [atom.index for atom in self._atom.bonded_atoms]
[docs]class FEPScholarMapper(object): """ This is a mapper class used by FEP Scholar GUI so that we don't need to use Canvas MCS for matching molecule atoms. """
[docs] def __init__(self, st1, st2, dist=0.40, match1=None, match2=None, core_hop=False): """ Class initializer, which takes two molecule structures that need to be matched. If optional core atom matches for two molecules are provided (match1 and match2) this data would be used to match two molecules instead. :param st1: first structure :type st1: `structure.Structure` :param st2: second structure :type st2: `structure.Structure` :param dist: distance cutoff :type dist: float :param match1: list of core atom indices for first structure :type match1: list :param match2: list of core atom indices for second structure :type match2: list """ self.st1 = st1.copy() self.st2 = st2.copy() self.match1 = match1 self.match2 = match2 self.core_hop = core_hop self.DISTANCE_CUTOFF = dist self.unmatched_atoms = [] self.carat_mapping = None self.createMap()
[docs] def createMap(self): """ This function should be called when this class is initialized. It determines atom mapping between two structures. Mapping information is stored as various properties in structure objects. """ self.clear_atom_props(self.st1) self.clear_atom_props(self.st2) # if core matches for two molecules are provided no calculation # is done and these matches are used instead to map molecules. if self.match1 is not None and self.match2 is not None: self.st2 = self._get_fep_mapping_from_matches(self.st2) else: self.carat_mapping = self.get_carat_map(self.st1) self.st2 = self.get_fep_mapping(self.st2) self._remove_partial_ring_match() self.match_core_hydrogens(self.st2) # set unmapped atom values as zero for atom in self.st2.atom: if constants.FEP_MAPPING not in atom.property: atom.property[constants.FEP_MAPPING] = 0 # assign substitution codes from schrodinger.application.desmond import fep_mapping fep_mapping.get_subst_from_mapping(self.st1, self.st2) self.st1.property[constants.FEP_FRAGNAME] = 'none' self.st2.property[constants.FEP_FRAGNAME] = 'lig2' r_group_asl.set_hotregion([self.st1, self.st2])
[docs] def getMatches(self): """ This function returns tuple that contains lists of 'core' atom indices for both structures. For the second ligand list of atom matches is reordered to reference the list of matches for the first ligand. :return: tuple that contains two lists of core atoms :rtype: tuple """ match1 = self._getMatches(self.st1, None) match2 = self._getMatches(self.st2, match1) return match1, match2
def _remove_partial_ring_match(self): """ This function takes the two structures and their atom mapping and removes mapping of atoms that are in partial rings or in alicyclic substructure """ if self.core_hop: return mol1 = SchrodStruc(self.st1) mol2 = SchrodStruc(self.st2) match1 = self._getMol1CoreAtomsMapping() match2 = self._getMol2CoreAtomsMapping() atom_mapping_mol1_to_mol2 = dict(list(zip(match1, match2))) for atom in mol1.atom: mol1.atom_prop[atom.index]['orig_index'] = atom.index core_mol = mol1.extract(match1) for i in range(1, core_mol._struc.atom_total + 1): orig_idx = core_mol.atom_prop[i]["orig_index"] mapped_idx = atom_mapping_mol1_to_mol2[orig_idx] core_mol.atom_prop[i]["mapped_index"] = mapped_idx core_mol_orig = core_mol.copy() partial_ring = rules._delete_broken_ring(mol1, mol2, core_mol) partial_ring += rules._delete_aromatic_alicyclic_matching( mol1, mol2, core_mol) self._update_st2_props(core_mol_orig, partial_ring) def _update_st2_props(self, core_mol_orig, remove_atoms): """ Remove i_fep_mapping property from st2 structure of those atoms that were matched to broken rings and alicyclic matching. :type core_mol_orig: `SchrodStruc` :param core_mol_orig: core molecule before trimming partial ring/alicycle matching :type remove_atoms: `int` :param remove_atoms: list of atoms in core_mol_orig that should be removed from the core """ for iatom in remove_atoms: remove_idx = core_mol_orig.atom_prop[iatom]['mapped_index'] del self.st2.atom[remove_idx].property[constants.FEP_MAPPING] def _getMol1CoreAtomsMapping(self): match = [] for atom in self.st2.atom: if atom.element != 'H' and constants.FEP_MAPPING in atom.property and \ atom.property[constants.FEP_MAPPING] != 0: match.append(atom.property[constants.FEP_MAPPING]) return match def _getMol2CoreAtomsMapping(self): match = [] for atom in self.st2.atom: if atom.element != 'H' and constants.FEP_MAPPING in atom.property and \ atom.property[constants.FEP_MAPPING] != 0: match.append(atom.index) return match def _getMatches(self, st, order_by=None): """ This function returns a list of 'core' atoms for a given structure. It takes another list as an optional argument, which is used to order the list of core atoms. :param st: ligand structure :type st: `structure.Structure` :param order_by: list of 'reference' structure core atoms which defines order of core atoms for this structure. :type order_by: list :return: list of 'core' atoms for a given structure :rtype: list """ match = [] d = {} for atom in st.atom: if atom.property[constants.FEP_MAPPING] != 0: if order_by is None: match.append(int(atom)) else: d[atom.property[constants.FEP_MAPPING]] = int(atom) if order_by is not None: match = [d[x] for x in order_by] return match
[docs] def getMutations(self): """ This function finds mutated atoms for two ligand structures. Atoms in two ligands are defined as 'mutated' if they are defined as 'core' atoms, but don't have same atomic numbers. :return: tuple that contains lists of 'mutated' atoms for two ligands :rtype: tuple """ match1, match2 = self.getMatches() unmatch1 = set() unmatch2 = set() for i1, i2 in zip(match1, match2): an1 = self.st1.atom[i1].atomic_number an2 = self.st2.atom[i2].atomic_number if an1 != an2: unmatch1.add(i1) unmatch2.add(i2) return unmatch1, unmatch2
[docs] def getHotAtoms(self): """ This function returns lists of 'hot' atoms for two ligands. :return: tuple that contains lists of 'hot' atoms. :rtype: tuple """ hot1 = [ int(a) for a in self.st1.atom if a.property[constants.REST_HOTREGION] == 1 ] hot2 = [ int(a) for a in self.st2.atom if a.property[constants.REST_HOTREGION] == 1 ] return hot1, hot2
[docs] def getStructures(self): """ This function returns `structure.Structure` objects for two ligands. Note that these structures are different(!) from the ones used to initialize this class. There contain special fep mapping properties. :return: modified structures for two ligands :rtype: tuple """ return self.st1, self.st2
[docs] def getCoreRmsd(self): """ This function calculates RMSD for core atoms in two ligands. :return: core RMSD :rtype: float """ match1, match2 = self.getMatches() if len(match1) < 3 or len(match2) < 3: return None return rmsd.calculate_in_place_rmsd(self.st1, match1, self.st2, match2)
#======================================================================== # These functions were adopted from the Dima's original script. They are # used to establish atom mapping between two ligands. #========================================================================
[docs] def get_carat_map(self, st): coordinates_dict = {} for atom in st.atom: if atom.element == 'H': continue key = self.get_atom_key(atom) coordinates_dict[key] = MetaAtom(atom) return coordinates_dict
[docs] def clear_atom_props(self, st): for atom in st.atom: for p in [ constants.FEP_SUBST, constants.ORIGINAL_INDEX, constants.FEP_SUBST ]: if p in atom.property: del atom.property[p]
[docs] def get_dist(self, xyz1, xyz2): x = xyz1[0] - xyz2[0] y = xyz1[1] - xyz2[1] z = xyz1[2] - xyz2[2] return math.sqrt(x * x + y * y + z * z)
[docs] def get_atom_key(self, atom): xyz = atom.xyz return '{0:0.1f} {1:0.1f} {2:0.1f}'.format(xyz[0], xyz[1], xyz[2])
[docs] def get_coords_dict(self, st): """ This function creates so-called coordinates dictionary for each atom in a structure. Here key is the text string based on atom coordinates and value is atom's MetaAtom object. :return: 'coordinates dictionary' :rtype: dict """ coordinates_dict = {} for atom in st.atom: key = self.get_atom_key(atom) coordinates_dict[key] = MetaAtom(atom) return coordinates_dict
[docs] def del_from_carat(self, idx): for key, atom in future.utils.listitems(self.carat_mapping): if atom.index == idx: del self.carat_mapping[key] break
[docs] def set_match_atom(self, atom, matched_atom_index): if constants.FEP_MAPPING in atom.property and \ atom.index != atom.property[constants.FEP_MAPPING] and \ atom.property[constants.FEP_MAPPING] != matched_atom_index: print("WARNING: Cannot match atom %i to %i, as it's already matched to %i." \ % (atom.index, atom.property[constants.FEP_MAPPING], matched_atom_index)) return atom.property[constants.FEP_MAPPING] = matched_atom_index if matched_atom_index != 0: # print "setting...", atom.index,matched_atom_index self.del_from_carat(matched_atom_index) try: self.unmatched_atoms.remove(atom.index) except ValueError: pass
[docs] def match_by_cartesian_coordinates(self, st_new): for atom in st_new.atom: if atom.element == 'H': continue key = self.get_atom_key(atom) if key in self.carat_mapping: self.set_match_atom(atom, self.carat_mapping[key].index) else: self.unmatched_atoms.append(atom.index)
[docs] def match_by_distance(self, st_new, distance): for ua in self.unmatched_atoms[:]: for key in list(self.carat_mapping): dist = self.get_dist(st_new.atom[ua].xyz, self.carat_mapping[key].xyz) # print "attempting to match...", ua, self.carat_mapping[key].index, dist, (dist < distance) if dist < distance: self.set_match_atom(st_new.atom[ua], self.carat_mapping[key].index) break
[docs] def get_core_map(self, st_new): core_map = [] for atom in st_new.atom: if constants.FEP_MAPPING in atom.property: ifepmapping = atom.property[constants.FEP_MAPPING] if ifepmapping: core_map.append((ifepmapping, atom.index)) return core_map
[docs] def remove_unconn_core_atoms(self, st_new): core_map = self.get_core_map(st_new) print(core_map) for (a1, a2) in core_map: na1_core = [ a for a in self.st1.atom[a1].bonded_atoms if (constants.FEP_MAPPING in a.property) ] na2_core = [ a for a in st_new.atom[a2].bonded_atoms if (constants.FEP_MAPPING in a.property) ] if not (na1_core or na2_core): st_new.atom[a2].property[constants.FEP_MAPPING] = None print(self.get_core_map(st_new))
[docs] def match_core_hydrogens(self, st_new): if self.core_hop: return for a0, a1 in self.get_core_map(st_new): hydro0 = [ e for e in self.st1.atom[a0].bonded_atoms if (e.element == 'H') ] hydro1 = [ e for e in st_new.atom[a1].bonded_atoms if (e.element == 'H') ] for h0 in hydro0: dist = [] for h1 in hydro1: dist.append(self.get_dist(h0.xyz, h1.xyz)) if dist: i_h1_match = dist.index(min(dist)) hydro1[i_h1_match].property[ constants.FEP_MAPPING] = h0.index hydro1.pop(i_h1_match)
[docs] def get_fep_mapping(self, st): st_new = st.copy() self.match_by_cartesian_coordinates(st_new) self.match_by_distance(st_new, self.DISTANCE_CUTOFF) self.match_by_distance(st_new, self.DISTANCE_CUTOFF * 2) # remove matched atoms that are not connected to the core atoms # self.remove_unconn_core_atoms(st_new) return st_new
def _get_fep_mapping_from_matches(self, st): """ This function is called to set up mapping properties in a given structure from matching core atom indices provided at class initialization. :param st: structure for which FEP mapping properties need to be set :type st: `structure.Structure` :return: copy of input structure with FEP mapping properties :rtype: `structure.Structure` """ st_new = st.copy() # initialize atom i_fep_mapping values to zero for atom in st_new.atom: atom.property[constants.FEP_MAPPING] = 0 for m1, m2 in zip(self.match1, self.match2): st_new.atom[m2].property[constants.FEP_MAPPING] = m1 self.match_core_hydrogens(st_new) return st_new
[docs]class ScholarStructureItem(structure2d.structure_item): """ This class adds new function to the base structure_item class that allows to show some atoms and bonds with highlighting. """
[docs] def generatePictureHighlight(self, atoms, bonds, color, gen_coord): """ Generates a QPicture of the structure. This should be called after setting the structure and the accompaning annotators. :param atoms: list atom indices that should be highlighted. :type atoms: list :param bonds: list of bond indices that should be highlighted :type bonds: list :param color: highlighting color :type color: `QtGui.QColor` :param gen_coord: argument indicating whether molecule coordinates should be generated. :type gen_coord: bool """ if self.colormap: self.model2d.setColorMap(self.colormap) for ann in self.annotators: self.model2d.addAnnotator(ann) self.renderer.setModel(self.model2d) renderer_rect = canvas2d.ChmRect2D(self.rect.x(), self.rect.y(), self.rect.width(), self.rect.height()) self.renderer.setBoundingBox(renderer_rect) self.pic = structure2d.get_qpicture_highlight(self.renderer, self.chmmol, atoms, bonds, color, gen_coord) ret = [] for func in self.annotator_function_returns: ret.append(func) #cleanup if self.colormap: self.model2d.clearColorMap() self.model2d.clearAnnotators() self.update() return ret
[docs]class FEPStructureObject(QtCore.QObject): """ This class holds all information about each ligand. It also contains drawing objects. """
[docs] def __init__(self, parent, rect=None): QtCore.QObject.__init__(self, parent) self.st = None self.scene = structure2d.structure_scene() self.item = ScholarStructureItem(rect) self.scene.addItem(self.item) self.view = structure2d.structure_view(self.scene) self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.view)
[docs] def setStructure(self, st, template_chmmol=None, mapper=None): """ Set structure and generate coordinates. If template molecule is provided, coordinates will be generated to align matching mcs atoms. :param st: molecule structure :type st: structure.Structure :param template_chmmol: template molecule :type template_chmmol: canvas2d.ChmMol :param mapper: FEP mapper from which MCS matches are obtained. :type mapper: FEPScholarMapper """ self.st = st self.item.clear_annotators() self.item.set_structure(st) if template_chmmol is not None and mapper is not None: match1, match2 = mapper.getMatches() match = list(map(DECREMENT, match2)) template_match = list(map(DECREMENT, match1)) canvas2d.Chm2DCoordGen.generateFromTemplateAndApply( self.item.chmmol, template_chmmol, match, template_match, IGNORE_HYDROGEN, RESCALE, FIXUP) else: canvas2d.Chm2DCoordGen.generateAndApply( self.item.chmmol, canvas2d.ChmAtomOption.H_Never, canvas2d.ChmCoordGenOption.Rendering) self.item.model2d.setShowHydrogenPreference( canvas2d.ChmAtomOption.H_Never) self.item.generate_picture(gen_coord=False)
[docs] def setChmmol(self, chmmol): """ Assigns a chmmol to the structure item, then clears and regenerates its picture. :param chmmol: the chmmol object corresponding to the structure :type chmmol: canvas2d.ChmMol """ self.item.clear_annotators() self.item.chmmol = chmmol self.item.generate_picture(gen_coord=False)
[docs] def clear(self): self.item.clear() self.view.update()
[docs] def sizeHint(self): """ Provide size hint for structure pictures. See Qt documentation for an explanation of arguments and return value. """ return QtCore.QSize(250, 250)
[docs]def make_fep_cmd(cd_params, jobname, struct_fname, opt=[]): # noqa: M511 """ Generates an FEP command list based on the specified parameters. :param cd_params: config dialog parameters :type cd_params: dict :param jobname: the jobname :type jobname: str :param main_msj_fname: the filename for the main msj file :type main_msj_fname: str :param struct_fname: the filename for the input structure :type struct_fname: str :return: a command list for launching the job :rtype: list """ cmd = [ os.path.join(os.environ["SCHRODINGER"], "utilities", "multisim"), '-HOST', cd_params['host'], '-SUBHOST', cd_params['subjob_host'], '-m', jobname + '.msj' ] cmd += ['-JOBNAME', jobname, struct_fname] generate_scripts(cd_params, jobname, cmd) return cmd
[docs]def generate_scripts(cd_params, jobname, cmd): """ Write the command line for submitting the job """ def write_script(fname, cmd): with open(fname, 'w') as f: print(subprocess.list2cmdline(cmd), file=f) st = os.stat(fname) os.chmod(fname, st.st_mode | 0o111) #start script start_cmd = list(cmd) start_cmd[0] = "$SCHRODINGER/utilities/multisim" write_script("%s.sh" % jobname, start_cmd)