Source code for schrodinger.application.matsci.builderwidgets

"""
Common widgets and code for builders such as the TM Complex Builder and the
Oligamer builder

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

import abc
import glob
import os.path
from pathlib import Path

import inflect

import schrodinger
from schrodinger import structure
from schrodinger.application.matsci import buildcomplex
from schrodinger.application.matsci import coarsegrain
from schrodinger.application.matsci import msutils
from schrodinger.infra import mm
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.rdkit import rgroup
from schrodinger.structutils import analyze
from schrodinger.structutils import build
from schrodinger.structutils import minimize
from schrodinger.structutils import transform
from schrodinger.thirdparty import rdkit_adapter
from schrodinger.ui import sketcher
from schrodinger.ui.qt import decorators
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import filter_list
from schrodinger.ui.qt import structure2d
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import utils as qtutils
from schrodinger.utils import fileutils
from schrodinger.utils import preferences

maestro = schrodinger.get_maestro()

TEMPLATE_EXTENSIONS = {'.mae', '.mae.gz', '.maegz'}

TEMPLATE_EXTENSION = '.maegz'
TEMPLATE_GLOB = '*' + TEMPLATE_EXTENSION
RECENT_TEMPLATE_FILE = 'recent.txt'
DEFAULT_TEMPLATE = 'H'
CUSTOM_TEMPLATE = 'Custom'
NO_TEMPLATE = 'This is not a template'
# Temporary element for marker atoms because 'H' is not sticky (tends to get
# added/deleted when converting structures)
TEMP_ELEMENT = 'Br'
PREF_GROUP = 'general_builder_widgets'
RESELECT_CURRENT_TEMPLATE = 'reselect current template'
RESET_TO_DEFAULT_CUSTOM_DIR = 'reset to default custom dir'
NONE_ITEM = 'None'

MARKING_INFO = ('To mark an atom, either right click on it and choose "Set '
                'R group" or hover over it and type "Rx", where x is the '
                'desired R group number.')


[docs]def get_builtin_template_items(path, item_type, item_class=None): """ Get a list of the built-in templates :param str path: The path to the script (generated via __file__ property) :param str item_type: The type item these templates are. Will form the base name of the template directory. For example, polymer items might have types of "monomer" or "initiator" while complexes might have have a type of "ligand". :param type(QtWidgets.QListWidgetItem) item_class: The base class to use when creating the template items. Defaults to QtWidgets.QListWidgetItem :return tuple(item_class): A list of the builtin template items """ if item_class is None: item_class = QtWidgets.QListWidgetItem built_in_path = get_builtin_directory(path=path, item_type=item_type) items = [item_class(CUSTOM_TEMPLATE)] for file_path, _ in fileutils.get_files_from_folder(built_in_path): if fileutils.get_file_extension(file_path) in TEMPLATE_EXTENSIONS: template_name = Path(file_path).stem.replace('_', ' ') item = item_class(template_name) items.append(item) return tuple(items)
[docs]def get_builtin_directory(path, item_type): """ Given a path to a script and an item type, generate the path to built-in templates for that script and item_type If the script is located at /A/B/bob.py, the path returned will be /A/B/bob_dir/item_type_templates :type path: str :param path: The path to the script (generated via __file__ property) :type item_type: str :param item_type: The type item these templates are. Will form the base name of the template directory :rtype: str :return: The path to the built-in directory for templates of item_type for the script at path """ script_dir, script_name = os.path.split(os.path.abspath(path)) builtin_base_dir = os.path.join(script_dir, os.path.splitext(script_name)[0] + '_dir') built_in_path = os.path.join(builtin_base_dir, item_type + '_templates') return built_in_path
# The following constants and functions were moved out this module so they could # be used in processes that can't import modules requiring a display. They are # re-defined here to preserve this module's API so that any uses in the wild are # preserved. HIGHEST_RX_MARKER_XVAL = buildcomplex.HIGHEST_RX_MARKER_XVAL ATOM_MARKER_PROP_BASE = buildcomplex.ATOM_MARKER_PROP_BASE ETA_ATOMS_PROP = buildcomplex.ETA_ATOMS_PROP get_marker_atom_indexes_from_structure = \ buildcomplex.get_marker_atom_indexes_from_structure mark_eta_positions = buildcomplex.mark_eta_positions get_eta_marker_indexes = buildcomplex.get_eta_marker_indexes clear_marker_properties = buildcomplex.clear_marker_properties set_marker_properties = buildcomplex.set_marker_properties
[docs]class TemplateSelectorFilterListPopUp(filter_list.FilterListPopUp): """ A pop up widget that allows for dynamic filtering and selection of template structures. """
[docs] def __init__(self, parent): """ :param QtWidgets.QWidget parent: parent widget """ list_items = self._getListItems() filter_cbs = self._getFilterCheckBoxes() super().__init__( parent, list_items, filter_cbs, toggle_filtering_text='Limit list to matching templates')
@abc.abstractmethod def _getListItems(self): """ Meant to get the list items used to instantiate this class :return tuple(QListWidgetItems): A tuple of the list items for this list widget """ raise NotImplementedError @abc.abstractmethod def _getFilterCheckBoxes(self): """ Meant to get the checkboxes used to instantiate this class :return tuple(filter_list.FilterCheckBox): A tuple of the filter checkboxes for this list widget """ raise NotImplementedError
[docs] def getSelection(self): """ Get the currently selected template :return str or None: String indicating the name of the template or `None` if no template is selected. """ template = self._list_widget.currentItem() if template: return template.text() else: return None
[docs] def setSelection(self, template): """ Set the template to the requested value. :param str template: The name of the template to set the pop up to. :raise ValueError: If the requested template name was not found. In these cases, the template will not be changed and `template_changed` will not be emitted. :raise RuntimeError: If more than one matching template was found. In these cases, the template will not be changed and `template_changed` will not be emitted. """ items = self._list_widget.findItems(template, Qt.MatchFlag.MatchFixedString) n_items = len(items) if n_items == 0: raise ValueError(f"Template not found: {template}") elif n_items > 1: raise RuntimeError('More than one matching template found for ' f'{template}') self._list_widget.setCurrentItem(items[0])
[docs]class TemplateSelectorFilterListToolButton( filter_list.ToolButtonWithFilterListPopUp): """ Custom tool button with a template structure selector filter list pop up. """ # Should be a child class of TemplateSelectorFilterListPopUp POP_UP_CLASS = None
[docs] def getSelection(self): return self._pop_up.getSelection()
[docs] def setSelection(self, template): self._pop_up.setSelection(template)
[docs]class TemplateSelector(swidgets.SelectorWithPopUp): """ A frame that allows the user to specify a template structure from a pop up list. """ # Should be a child class of TemplateSelectorFilterListToolButton TOOL_BUTTON_CLASS = None
[docs] def __init__(self, *args, item_class=None, command=None, **kwargs): """ Extend super's __init__ by assigning a self.item_class attribute. This attribute is the base class used when creating/updating the item list in this class' list widget. :param type(QtWidgets.QListWidgetItem) item_class: The child class of QListWidgetItem to use when updating or resetting items in the list widget. If `None`, then defaults to the base QListWidgetItem class. :param function command: function to call when the selector's tool button's pop up's data are changed """ if item_class is None: item_class = QtWidgets.QListWidgetItem self.item_class = item_class super().__init__(*args, **kwargs) if command: self.tool_btn._pop_up.dataChanged.connect(command)
[docs] def selectionChanged(self): """ Set the line edit to the newly selected template name """ self.selection_le.setText(str(self.tool_btn.getSelection()))
[docs] def getSelection(self): return self.tool_btn.getSelection()
[docs] def setSelection(self, template): """ Set the template name for the widget :param str template: The name of the template structure to set :raise ValueError: If the template name is not recognized """ self.tool_btn.setSelection(template) self.selection_le.setText(self.tool_btn.getSelection())
[docs] def reset(self): """ Reset the widget """ self.setSelection(self.default_selection)
@property def _list_widget(self): """ The list widget of the pop up attached to the tool button attached to this selector :rtype TemplateSelectorFilterListPopUp: """ return self.tool_btn._pop_up._list_widget
[docs] def silentlySetTemplates(self, template_names): """ Set the items for the selector without emitting any signals :param list(str) template_names: A list of the new template names for the selector """ with qtutils.suppress_signals(self._list_widget): self._list_widget.clear() for name in template_names: item = self.item_class(name) self._list_widget.addItem(item)
[docs] def silentlyTryToSelectTemplate(self, text): """ Select the item with the given text and do so without emitting any signals. Also do not give an error if no item with text exists. :param str text: The text of the item to select """ with qtutils.suppress_signals(self, self._list_widget): try: self.setSelection(text) except ValueError: pass
[docs]class TemplateCombo(swidgets.SComboBox): """ A Combo Box for managing template lists """
[docs] def __init__(self, *args, **kwargs): """ See parent class for documentation """ maxwidth = kwargs.pop('max_width', 450) swidgets.SComboBox.__init__(self, *args, **kwargs) self.setSizeAdjustPolicy(self.AdjustToContents) self.setMaximumWidth(maxwidth)
[docs] def silentlySetItems(self, items): """ Set the items for the selector without emitting any signals :type items: list :param items: The new items (str) for the combo box """ with qtutils.suppress_signals(self): self.clear() self.addItems(items)
[docs] def silentlyTryToSelectText(self, text): """ Select the item with the given text and do so without emitting any signals. Also do not give an error if no item with text exists. :type text: str :param text: The text of the item to select """ with qtutils.suppress_signals(self): try: self.setCurrentText(text) except ValueError: pass
[docs]class TemplateManager(QtWidgets.QDialog): """ A dialog that manages the user templates - currently only allows deletion """ customDirChanged = QtCore.pyqtSignal(str)
[docs] def __init__(self, parent, templates, custom_path): """ Create a TemplateManager object :type parent: QWidget :param parent: The window to display this dialog over :type templates: list :param templates: list of template names in the self.parent.custom_path directory :type custom_path: str :param custom_path: The directory where the custom templates are stored """ QtWidgets.QDialog.__init__(self, parent) self.setWindowTitle('Manage Templates') layout = swidgets.SVBoxLayout(self) layout.setContentsMargins(6, 6, 6, 6) # Template deletion dlayout = swidgets.SHBoxLayout(layout=layout) self.select_combo = swidgets.SLabeledComboBox('Template:', items=templates, layout=dlayout) swidgets.SPushButton('Delete Template', command=self.deleteTemplate, layout=dlayout) dlayout.addStretch() # Custom template directory setting clayout = swidgets.SHBoxLayout(layout=layout) swidgets.SPushButton('Change Custom Template Directory...', command=self.changeTemplateDirectory, layout=clayout) swidgets.SPushButton('Use Default Directory', command=self.useDefaultTemplateDirectory, layout=clayout) self.custom_path = custom_path
[docs] def deleteTemplate(self): """ Delete the selected template """ template_basename = str(self.select_combo.currentText()) if not template_basename: return filename = os.path.join(self.custom_path, template_basename + TEMPLATE_EXTENSION) fileutils.force_remove(filename) self.select_combo.removeItem(self.select_combo.currentIndex())
[docs] def changeTemplateDirectory(self): """ Change the directory the custom templates are read from/saved to """ path = filedialog.get_existing_directory(dir=self.custom_path) if not path: return self.customDirChanged.emit(path) # Close because our data is no longer valid self.accept()
[docs] def useDefaultTemplateDirectory(self): """ Switch the custom template directory back to the default directory """ self.customDirChanged.emit(RESET_TO_DEFAULT_CUSTOM_DIR) # Close because our data is no longer valid self.accept()
[docs]class SketcherStructureMixin(object): """ Contains some general use methods for getting a ligand structure from the 2D sketcher """
[docs] def changeMarkerAtomsToTempElement(self, struct, rx_atoms=None): """ Change all the marker atoms in the structure to a specific element :type struct: `schrodinger.structure.Structure` :param struct: The structure to modify :type rx_atoms: dict :param rx_atoms: A dictionary with keys that are the x value for Rx atoms and values that are lists of atom indexes with that x value. If not supplied, the Rx atoms will be found from structure properties. """ if msutils.is_coarse_grain(struct): # Coarse grained structure causes RuntimeError: Could not clear # isotope when changing Atomic number in buildcomplex.transmute_atom return if not rx_atoms: rx_atoms, max_x = get_marker_atom_indexes_from_structure(struct) for indexes in rx_atoms.values(): for index in indexes: buildcomplex.transmute_atom(struct.atom[index], TEMP_ELEMENT)
[docs] def getSketcherStructure(self, quiet_if_empty=False): """ Get the structure from the sketcher and set it up for use as a Ligand Performs the following manipulations: - Stores the index of the R1/R2 marker atoms as structure properties - Changes R1/R2 to hydrogen atoms - Checks to ensure consistent use of Rx in the structure :type quiet_if_empty: bool :param quiet_if_empty: If True, do not post a warning if the sketcher contains no structure :rtype: `schrodinger.structure.Structure` or None :return: The structure object from the sketcher, or None if an error occured along the way """ multistruct_msg = ('There must be one and only one molecule in the ' '2D Sketcher when setting the structure.') # Get the structure and Rx atoms rdmol = self.sketcher.getRDKitStructure() if not rdmol or not rdmol.GetNumAtoms(): if not quiet_if_empty: self.warning(multistruct_msg) # None or more than 1 structure in the sketcher return None rx_atoms, max_x = self.getRxAtoms() # Validate that we have the correct number of Rx atoms valid = self.validateRAtomIdentity(rx_atoms, max_x) if not valid: return None # Check if all elements are supported (MATSCI-11496) if any(a.GetAtomicNum() > mm.MMELEMENTS_MAX for a in rdmol.GetAtoms()): self.warning('Elements with atomic numbers larger than ' f'{mm.MMELEMENTS_MAX} are not supported.') return # Regenerate valences and allow odd valence states (MATSCI-4524) rdmol.UpdatePropertyCache(strict=False) # Convert rdmol to a structure struct = rdkit_adapter.from_rdkit(rdmol) # Set dummy atoms' atomic numbers to 0 before calling fast3d # (Workaround until SHARED-7523 is implemented) for atom in struct.atom: if atom.atomic_number == -2: atom.atomic_number = 0 try: struct.generate3dConformation(require_stereo=False) except RuntimeError as err: self.warning('Unable to generate 3D coordinates for ligand\n' + str(err)) if struct.mol_total != 1: self.warning(multistruct_msg) return set_marker_properties(struct, rx_atoms) # Change the atom to the Temp Element - it may not persist if it is a # hydrogen self.changeMarkerAtomsToTempElement(struct, rx_atoms=rx_atoms) # Validate Rx atoms based on the whole structure valid = self.validateRAtomStructure(struct, rx_atoms, max_x) if not valid: return None return struct
[docs] def getRxAtoms(self): """ Find the atoms marked as R groups in the sketcher structure :rtype: (dict, int) :return: dict keys are the int value of x in Rx, values are lists of atom indexes set to that Rx value (atom indexes are 1-based). The int return value is the largest value of x in the dictionary keys. """ # Figure out which atoms are marked as R-Groups rgroups = self.sketcher.listOfAtomRGroups() max_x = 0 rx_atoms = {} # Sketcher atoms are indexed starting at 0 for index, rval in enumerate(rgroups, 1): if rval > 0: rx_atoms.setdefault(rval, []).append(index) if rval > max_x: max_x = rval return rx_atoms, max_x
[docs] def validateRAtomIdentity(self, rx_atoms, max_x): """ Overwrite in a child class to run validation on the values of the R atoms and error code. An error message should be displayed to the user by this method if appropriate :type rx_atoms: dict :param rx_atoms: keys are the int value of x in Rx, values are lists of atom indexes set to that Rx value (atom indexes are 1-based) :type max_x: int :param max_x: The larget value of x in the keys of rx_atoms :rtype: bool :return: True if everything is OK, False if not """ return True
[docs] def validateRAtomStructure(self, struct, rx_atoms, max_x): """ Overwrite in a child class to run validation on the R atoms that requires a `schrodinger.structure.Structure` object. An error message should be displayed to the user by this method if appropriate. :type struct: `schrodinger.structure.Structure` :param struct: The structure to use for validating the Ra atoms :type rx_atoms: dict :param rx_atoms: keys are the int value of x in Rx, values are lists of atom indexes set to that Rx value (atom indexes are 1-based) :type max_x: int :param max_x: The larget value of x in the keys of rx_atoms :rtype: bool :return: True if everything is OK, False if not """ return True
[docs]class MinimizeMixin(object): """ Methods to convert a 2D structure to a 3D structure and minimize it """
[docs] def convert2DTo3DAndMinimize(self): """ Minimize the structure, converting from 2D to 3D first if necessary """ if not self.structure.has3dCoords(): # Convert to 3D self.structure.generate3dConformation(require_stereo=False) # We use OPLS 2005 to avoid errors with QM charges for our # modified ligand structures. (MATSCI-4008). We call with # cleanup = False to avoid mmlewis issues with the marker atoms # - some of which should be zero-bonded and some single-bonded # in order to get the valence totally correct. MATSCI-4524 try: with minimize.minimizer_context(ffld_version=minimize.OPLS_2005, cleanup=False, struct=self.structure) as mizer: self.modifyMinimizer(mizer) mizer.minimize() except Exception as err: msg = str(err) + '\n\nUsing unminimized ligand structure' if "mmffld_minimize_lic" in str(err): self.master.warning('A license error occurred:\n' + msg) else: self.master.warning('An error occurred during ' '3D structure minimization: ' + msg)
[docs] def findAttachmentMarkers(self): """ Find the atoms that mark attachment points. The index of each marker atom is stored in self.markers in ascending order of Rx value. """ marker_dict, max_x = get_marker_atom_indexes_from_structure( self.structure) # Find the attachment points self.markers = [] for xval in range(1, max_x + 1): self.markers.extend(marker_dict.get(xval, [])) self.eta_indexes = get_eta_marker_indexes(self.structure)
[docs]class TemplateNameDialog(swidgets.SDialog): """ A dialog to get a template name (or any name that forms a file name) """
[docs] def layOut(self): """ Lay out the widgets """ layout = self.mylayout self.edit = swidgets.SLineEdit( layout=layout, validator=swidgets.FileBaseNameValidator())
[docs] def accept(self): """ Ensure the text edit is valid, warn and do not close if not """ datatype = self.windowTitle().capitalize() datatype = inflect.engine().plural(datatype) if not self.edit.hasAcceptableInput(): self.warning(f'{datatype} must form valid file names') return None return super().accept()
[docs] def reject(self): """ Blank out the text edit if cancelled """ # Set the edit text to blank because the edit text is returned in the # getText method. self.edit.setText("") return super().reject()
[docs] @classmethod def getText(cls, master, title='Template Name'): """ Open an instance of the dialog and return the result :rtype: str :return: The specified filename. A blank string is returned if the user cancelled. """ dlg = cls(master, title=title) dlg.exec() return dlg.edit.text()
[docs]class SketcherBox(SketcherStructureMixin, swidgets.SFrame): """ Set of widgets that controls a 2D sketcher and has additional widgets that allow the user to load/save/delete templates for the sketcher and import structures from the workspace into the sketcher. """ templatesUpdated = QtCore.pyqtSignal((list, str))
[docs] def __init__(self, master, builtin_path, custom_dirname, layout, add_custom_template=True, single_rx_atom=False): """ Create a SketcherBox instance :type master: QWidget :param master: Must have a warning method :type builtin_path: str :param builtin_path: The absolute path to the built-in templates :type custom_dirname: str :param custom_dirname: The base name of the path to the custom templates :type layout: QBoxLayout :param layout: The layout to place this widget into :type add_custom_template: bool :param add_custom_template: Whether "Custom" template should be allowed in template list :param single_rx_atom: If True, there can only be one single rx atom :type single_rx_atom: bool """ self.master = master self.add_custom_template = add_custom_template self.single_rx_atom = single_rx_atom self.warning = master.warning self.single_rx_templates = {} swidgets.SFrame.__init__(self, layout_type=swidgets.VERTICAL, layout=layout) # Preferences self.prefs = preferences.Preferences(preferences.SCRIPTS) self.prefs.beginGroup(PREF_GROUP) self.custom_dir_basename = custom_dirname # Template widgets self.template_frame = swidgets.SFrame(layout=self.mylayout, layout_type=swidgets.HORIZONTAL) tlayout = self.template_frame.mylayout self.leftmost_label = swidgets.SLabel('Template:', layout=tlayout) self.template_combo = TemplateCombo(layout=tlayout) self.template_combo.currentIndexChanged.connect(self.loadTemplate) self.save_template_btn = swidgets.SPushButton('Save New Template...', command=self.saveTemplate, layout=tlayout) self.manage_template_btn = swidgets.SPushButton( 'Manage Templates...', command=self.manageTemplates, layout=tlayout) tlayout.addStretch() # Fill the template combo self.builtin_path = builtin_path default_custom_dir = self.getDefaultCustomTemplateDir() self.setCustomTemplateDir( self.prefs.get(custom_dirname, default=default_custom_dir)) try: os.makedirs(self.custom_path) except OSError: # Directory already exists (or we can't create it) pass self.templates = [] self.builtin_templates = set() self.buildTemplateList() # Other custom sketcher controls clayout = swidgets.SHBoxLayout(layout=self.mylayout) if maestro: self.workspace_btn = swidgets.SPushButton( 'Import From Workspace', command=self.importWorkspace, layout=clayout) clayout.addStretch() self.clear_btn = swidgets.SPushButton('Clear Sketcher', command=self.clearSketcher, layout=clayout) # Sketcher self.sketcher = sketcher.sketcher() self.mylayout.addWidget(self.sketcher) # Tooltips tip = 'Load a template into the Sketcher' self.template_combo.setToolTip(tip) tip = 'Save a new template for use in future sessions.' self.save_template_btn.setToolTip(tip) tip = 'Delete one or more custom templates' self.manage_template_btn.setToolTip(tip) tip = 'Clear the Sketcher of all structures' self.clear_btn.setToolTip(tip)
[docs] def filterTemplates(self): """ Filter templates based on whether single rx atom is requested. :return: valid templates :rtype: list """ if not self.single_rx_atom and self.template_combo.count() == len( self.templates): return templates = self.templates[:] if self.single_rx_atom: for template in self.templates: if template == CUSTOM_TEMPLATE: self.single_rx_templates[template] = True continue if template in self.single_rx_templates: continue struct = self.readTemplateStructure(template) midxs, _ = get_marker_atom_indexes_from_structure(struct) self.single_rx_templates[template] = sum( [len(x) for x in midxs.values()]) == 1 templates = [x for x, y in self.single_rx_templates.items() if y] self.template_combo.silentlySetItems(templates) return templates
[docs] def getDefaultCustomTemplateDir(self): """ Get the default directory for user templates """ return os.path.join(msutils.get_matsci_user_data_dir(), self.custom_dir_basename)
[docs] def setCustomTemplateDir(self, path): """ The user changed the custom template directory :type path: str :param path: The new custom directory path. Use the module constant RESET_TO_DEFAULT_CUSTOM_DIR to reset to the Schrodinger default """ if path == RESET_TO_DEFAULT_CUSTOM_DIR: path = self.getDefaultCustomTemplateDir() self.custom_path = path self.prefs.set(self.custom_dir_basename, path)
[docs] def getRecentTemplatePath(self): """ Get the path to the recent template file :rtype: str :return: The path to the recent template file """ return os.path.join(self.custom_path, RECENT_TEMPLATE_FILE)
[docs] def importWorkspace(self): """ Get the workspace structure and place it in the sketcher """ struct = maestro.workspace_get() if msutils.is_coarse_grain(struct, by_atom=True): self.warning('Coarse-grained systems cannot be used as ligands') return # Check for dummy atoms - a single dummy atom indicates a ligand binding # point. A dummy atom bound to another dummy is just a dummy - probably # from the Maestro method of representing eta-ligands (dummy out of # plane attached to a dummy in-plane at the center of the eta atoms) rxinfo = [] rxval = 1 delatoms = [] dummy = 'DU' for atom in struct.atom: if atom.element.upper() == dummy: if (atom.bond_total == 1 and next(atom.bonded_atoms).element.upper() == dummy): delatoms = [atom.index] else: rxinfo.append((rxval, atom)) rxval += 1 # Below call is harmless if there are no atoms to delete struct.deleteAtoms(delatoms) # Create the rx atom dict after deleting atoms to ensure the atom # indexes are correct rx_atoms = {x: [atom.index] for x, atom in rxinfo} self.setSketcherStructure(struct) if rx_atoms: self.setAtomRNumbers(struct, rx_atoms)
[docs] def clearSketcher(self): """ Remove the current structure from the sketcher """ self.sketcher.clearStructure() self.template_combo.setCurrentIndex(0)
[docs] def buildTemplateList(self, select=""): """ Build a list of all templates - built in or user-defined. This list is stored in self.templates and replaces the current list in the Template Combobox. :type select: str :param select: The template that should be selected in the Template Combobox when all is said and done. """ # Preserve the original select setting so that we can pass it on to the # item row for use with its template combo. original_select = select if select == RESELECT_CURRENT_TEMPLATE: select = str(self.template_combo.currentText()) # First get the built-in templates self.builtin_templates = set() fileglob = os.path.join(self.builtin_path, TEMPLATE_GLOB) for filename in glob.iglob(fileglob): name = os.path.splitext(os.path.split(filename)[1])[0] self.builtin_templates.add(name) # Next get the user templates template_set = self.builtin_templates.copy() user_templates = self.getUserTemplateList() template_set.update(user_templates) # Now sort by recently used (alphabetical being the default) self.templates = list(template_set) self.templates.sort() recent = self.getRecentTemplatesList() def sortkey(name): try: return recent.index(name) except ValueError: if name in self.builtin_templates: # Place built-in templates after user templates return 10000 else: return 1000 self.templates.sort(key=sortkey) if self.add_custom_template: self.templates.insert(0, CUSTOM_TEMPLATE) self.template_combo.silentlySetItems(self.templates) self.template_combo.silentlyTryToSelectText(select) self.templatesUpdated.emit(self.templates, original_select)
[docs] def getUserTemplateList(self): """ Get the list of user templates :rtype: list :return: list of template names in the self.custom_path """ fileglob = os.path.join(self.custom_path, TEMPLATE_GLOB) template_names = [] for filename in glob.iglob(fileglob): name = os.path.splitext(os.path.split(filename)[1])[0] template_names.append(name) return template_names
[docs] def getRecentTemplatesList(self): """ Get a list of recently used templates. :rtype: list of str :return: Items of the list are the names of recently used templates. The list is sorted in order from most to least recently used. """ try: recent_file = open(self.getRecentTemplatePath()) used_list = [x.strip() for x in recent_file if x.strip()] recent_file.close() except IOError: used_list = [] return used_list
[docs] def updateRecentTemplateFile(self, name): """ Add a template as the most recently used template file. :type name: str :param name: The name of the most recently used template - should correspond to the name of the template structure file. """ recent_list = self.getRecentTemplatesList() try: recent_list.remove(name) except ValueError: pass recent_list.insert(0, name) try: recent_file = open(self.getRecentTemplatePath(), 'w') except IOError: return recent_str = '\n'.join(recent_list) recent_file.write(recent_str) recent_file.close()
[docs] def saveTemplate(self): """ Save the current sketcher structure as a new user-defined template """ struct = self.getSketcherStructure() if not struct: return # Get the template name template_name = TemplateNameDialog.getText(self) if template_name: if not os.path.exists(self.custom_path): try: os.makedirs(self.custom_path) except OSError: self.warning('Unable to create directory %s for user' ' template files.' % self.custom_path) return struct.title = template_name filename = template_name + TEMPLATE_EXTENSION path = os.path.join(self.custom_path, filename) try: struct.write(path) except IOError: self.warning('Could not write the template file\n%s' % path) return self.updateRecentTemplateFile(template_name) self.buildTemplateList(select=template_name) self.filterTemplates()
[docs] def readTemplateStructure(self, name): """ Read in a template file based name. The file name read is name + TEMPLATE_EXTENSION. :type name: str :param name: The base name of the template file without the extension :rtype: `schrodinger.structure.Structure` :return: The structure that was read in """ filename = name + TEMPLATE_EXTENSION if name in self.builtin_templates: directory = self.builtin_path else: directory = self.custom_path path = os.path.join(directory, filename) struct = structure.Structure.read(path) # Convert the R-Group marker atoms to the TEMP_ELEMENT - even though # they should be already, but files from early versions use 'F' which # makes the marker labels an ugly green. self.changeMarkerAtomsToTempElement(struct) return struct
[docs] def loadTemplate(self, template_index): """ Load a template into the sketcher :type template_index: int :param template_index: The index in the templates list of the template to load """ if not template_index: # Index = 0 is the Custom template - i.e. user draws the structure return # Find the file and load the structure template_name = self.templates[template_index] struct = self.readTemplateStructure(template_name) # Set the sketcher structure self.setSketcherStructure(struct) # Update the Template list self.updateRecentTemplateFile(template_name) self.buildTemplateList(select=template_name) # Find which atoms should be marked with Rx designations rx_atoms, max_x = get_marker_atom_indexes_from_structure(struct) if not rx_atoms: self.warning('No Rx-labeled coordination sites - not a valid ' 'template file.') return self.setAtomRNumbers(struct, rx_atoms)
[docs] def setAtomRNumbers(self, struct, rx_atoms): """ Set the Rnumbers based on the structure with Hydrogen atoms and the corresponding atom indexes marked with that Rx value. :type struct: structure.Structure :param struct: a monomer structure :type rx_atoms: dict :param rx_atoms: keys are Rx x values and values are lists of atom indexes marked with that Rx value """ # Hydrogens are not counted in the atom count for sketcher atoms, so # we have to reduce Rx indexes by the number of hydrogens that # come before them. hcounts = {} num_h = 0 for atom in struct.atom: hcounts[atom.index] = num_h if atom.element == 'H': num_h = num_h + 1 rdkit_mol = self.sketcher.getRDKitStructure() for xval, indexes in rx_atoms.items(): for index in indexes: index_without_h = index - hcounts[index] # Sketcher atoms are 0-indexed, so subtract another 1 rdkit_atom = rdkit_mol.GetAtomWithIdx(index_without_h - 1) rgroup.change_to_rgroup(rdkit_atom=rdkit_atom, rgroup_number=xval) self.sketcher.clearStructure() self.sketcher.setStructure(rdkit_mol, self.getSketcherImportSettings())
[docs] def getSketcherImportSettings(self): """ Returns sketcher import structure settings. :rtype: sketcher.sketcherImportStructureSettings :return: sketcher import structure settings for panel usage """ settings = sketcher.sketcherImportStructureSettings() settings.deleteExplicitHs = True return settings
[docs] def setSketcherStructure(self, struct, rm_polymer_prop=True): """ Set struct as the structure in the sketcher :type struct: structure.Structure :param struct: the structure to be placed in the 2D sketcher :type rm_polymer_prop: bool :param rm_polymer_prop: remove the s_matsci_polymer_role property for each atom, if True """ if rm_polymer_prop: msutils.remove_atom_property(struct, mm.M2IO_DATA_ATOM_POLYMER_ROLE) # Copied from MM_UIMSketcher::setStructure rdmol = rdkit_adapter.to_rdkit(struct, sanitize=False) self.sketcher.clearStructure() self.sketcher.setStructure(rdmol, self.getSketcherImportSettings())
[docs] def manageTemplates(self): """ Open a window to allow the user to manage existing templates """ user_templates = self.getUserTemplateList() manager = TemplateManager(self, user_templates, self.custom_path) manager.customDirChanged.connect(self.setCustomTemplateDir) manager.exec() self.buildTemplateList(select=RESELECT_CURRENT_TEMPLATE) self.filterTemplates()
[docs] def reset(self): """ Reset all widgets """ self.sketcher.clearStructure() self.template_combo.reset() self.buildTemplateList()
[docs]class ItemRow(MinimizeMixin, swidgets.SFrame): """ A row of control widgets for a ligand """
[docs] def __init__(self, master, row_layout, item_type, unset_tip): """ Create a ItemRow instance :type master: `schrodinger.ui.qt.appframework.AppFramework` :param master: The parent panel for this row. Must have the following methods: deleteRow, setWaitCursor, restoreCursor :type row_layout: QLayout :param row_layout: The layout this ItemRow should add itself to :type item_type: str :param item_type: A string describing the type of item this row is, such as "ligand" - this will be displayed to the user in labels and tooltips. Use the lowercase form of the word - it will be capitalized when necessary. :type unset_tip: str :param unset_tip: The tooltip for the label when no structure has been set. """ swidgets.SFrame.__init__(self, layout=row_layout, layout_type=swidgets.HORIZONTAL) self.master = master # Leading label cap_item_type = item_type.capitalize() self.label = swidgets.SLabel(cap_item_type + ': ', layout=self.mylayout) self.label.setToolTip( 'Each row controls the data for one %s structure' % item_type) # Status label self.status_label = StructureLabel(self, None, unset_tip) # Delete button self.delete_btn = swidgets.DeleteButton(command=self.delete) self.delete_btn.setMaximumWidth(30) self.delete_btn.setStyleSheet('QPushButton {color: red}') self.delete_btn.setToolTip('Delete this %s' % item_type) self.structure = None self.markers = [] self.eta_indexes = set()
[docs] def reset(self): """ Reset all widgets """ self.label.reset() self.status_label.unset() self.structure = None self.markers = [] self.eta_indexes = set()
[docs] def getMolecularFormula(self): """ Compute a molecular formula without the marker atoms :rtype: str :return: The Hill-type molecular formula (minus marker atoms) """ struct = self.structure.copy() struct.deleteAtoms(self.markers) formula = analyze.generate_molecular_formula(struct) return formula
[docs] def delete(self): """ Delete this row """ self.master.deleteRow(self) self.close()
[docs] def setWaitCursor(self): """ Change to a wait cursor - needed for the wait_cursor decorator """ self.master.setWaitCursor()
[docs] def restoreCursor(self): """ Change to a wait cursor - needed for the wait_cursor decorator """ self.master.restoreCursor()
[docs] def prepareStructureForMinimization(self): """ Prepare the structure for minimization. The default implementation just changes the marker atoms to H atoms. """ for index in self.markers: buildcomplex.transmute_atom(self.structure.atom[index], 'H')
[docs] def postTreatMinimizedStructure(self, marker_element=TEMP_ELEMENT): """ Perform any necessary actions on a just-minimized structure, perhaps undoing some of the prep done by prepareStructureForMinimization. The default changes the marker atoms back to TEMP_ELEMENT atoms. :type marker_element: str :param marker_element: The element to change the marker atom to (the atom that marks the attachment point and is discarded when the structure is added to a larger structure) """ for index in self.markers: buildcomplex.transmute_atom(self.structure.atom[index], marker_element)
[docs] def modifyMinimizer(self, mizer): """ This method allows subclasses to customize the Minimizer in any way they see fit. The structure has already been set for the Minimizer and is a temporary copy of self.structure. :type mizer: schrodinger.structutils.minimizer.Minimizer :param mizer: The Minimizer object that will minimize the structure. """
[docs] def getStructure(self): """ Get the structure that has been requested """ self.structure = self.master.getSketcherStructure()
[docs] def removeFFProperties(self): """ Remove any properties set on structure from force field minimization so that those properties don't propagate to any structure built from this structure. """ for prop in list(self.structure.property): if prop[1:].startswith('_ff_'): del self.structure.property[prop]
[docs] @decorators.wait_cursor def setStructure(self): """ Set the structure for this row. Including: - Find the marker atoms - optimize ligand structure for best complex binding """ self.status_label.unset() self.structure = None self.markers = [] self.eta_indexes = set() self.getStructure() if self.structure is None: return # Check for properly defined attachments self.findAttachmentMarkers() if not self.markers: return # Coarse grained doesn't support minimization if not msutils.is_coarse_grain(self.structure): # Do this before adding hydrogens because it converts marker atoms to H # or Dummies so we don't add H to them. self.prepareStructureForMinimization() # The sketcher structure doesn't include hydrogen atoms by default self.addHydrogens() self.convert2DTo3DAndMinimize() self.removeFFProperties() self.postTreatMinimizedStructure() self.fillStatusLabel()
[docs] def fillStatusLabel(self): """ Set the status label using the new structure """ self.status_label.set(self.structure)
[docs] def addHydrogens(self): """ Add hydrogens to the structure """ build.add_hydrogens(self.structure)
[docs]class StructureLabel(swidgets.SLabel): """ A label that gives information about the ligand for a given row and shows the set structure in a tooltip. """
[docs] def __init__(self, master, layout, unset_tooltip, unset_text="Not set", prefix="", suffix=""): """ Create a StructureLabel instance :type master: `ItemRow` :param master: The ItemRow object this label is for :type layout: QBoxLayout :param layout: The layout this label should add itself to :type unset_tooltip: str :param unset_tooltip: The tooltip to display if no structure has been set. :type unset_text: str :param unset_text: The text to display in the label if no structure has been set :type prefix: str :param prefix: The text to put before the molecular formula :type suffix: str :param suffix: The text to put after the molecular formula """ # Must set tooltip attribute at the very beginning, as it is used in the # event method, which gets called during some SLabel __init__ step. # MATSCI-1495 self.tooltip = None swidgets.SLabel.__init__(self, unset_text, layout=layout) self.master = master self.unset_text = unset_text self.unset_tip_text = unset_tooltip self.default_prefix = prefix self.default_suffix = suffix self.unset()
[docs] def unset(self): """ Change the text & tooltip to indicate there is no ligand set """ self.tooltip = None self.setText(self.unset_text) self.setToolTip(self.unset_tip_text)
[docs] def set(self, struct, prefix=None, suffix=None): """ Change the text & tooltip to indicate there is a ligand set :type struct: `schrodinger.structure.Structure` :param struct: The structure that is set for this row :type prefix: str :param prefix: A string to add before the molecular formala - if not given, the default prefix set in the __init__ method is used. :type suffix: str :param suffix: A string to add after the molecular formala - if not given, the default suffix set in the __init__ method is used. """ if prefix is None: prefix = self.default_prefix if suffix is None: suffix = self.default_suffix if suffix: suffix = ' ' + suffix if msutils.is_coarse_grain(struct): formula = coarsegrain.get_atom_name_formula( struct, excluded_atom_ids=self.master.markers) else: formula = self.master.getMolecularFormula() self.setText('%s%s%s' % (prefix, formula, suffix)) self.tooltip = RGroupStructureToolTip(struct)
[docs] def event(self, event): """ Override event to make the structure tooltip work :type event: QEvent :param event: The QEvent object generated by this event :rtype: bool :return: Whether the event was recognized """ if self.tooltip: if event.type() == event.ToolTip: self.tooltip.show() event.accept() return True return swidgets.SLabel.event(self, event)
[docs] def leaveEvent(self, event): """ Removes the structure tooltip if necessary when the mouse leaves the widget. :type event: QEvent :param event: The QEvent object generated by this event """ try: self.tooltip.finish() except AttributeError: swidgets.SLabel.leaveEvent(self, event)
[docs]class RGroupStructureToolTip(structure2d.StructureToolTip): """ A structure tooltip that displays Rx labels for the Rx atoms """
[docs] def __init__(self, struct): """ Create an RGroupStructureToolTip instance :type struct: `schrodinger.structure.Structure` :param struct: The structure to display """ scopy = struct.copy() labels = {} rx_atoms, max_x = get_marker_atom_indexes_from_structure(scopy) for xval, indexes in rx_atoms.items(): for index in indexes: labels[index] = ('R', str(xval)) if mm.mmct_atom_is_coarse_grain_particle(scopy, index): # Coarse grained structure causes RuntimeError: Could not clear # isotope when changing Atomic number with # WARNING : MMCT ERROR(-4): mmct_atom_set_isotope() continue buildcomplex.transmute_atom(scopy.atom[index], TEMP_ELEMENT) rx_annotator = structure2d.AtomLabelAnnotator(labels) structure2d.StructureToolTip.__init__(self, structure=scopy, annotators=[rx_annotator])
[docs]class SketchDialog(QtWidgets.QDialog): """ A Dialog window that opens a SketcherBox instance """
[docs] def __init__(self, master, sketcher, title): """ Create a SketchDialog instance :type master: `EndGroupRow` :param master: The EndGroupRow object this dialog belongs to :type sketcher: `schrodinger.application.matsci.builderwidgets.SketcherBox` :param sketcher: The SketcherBox to display :type title: str :param title: The window title """ self.master = master QtWidgets.QDialog.__init__(self, master) self.setWindowTitle(title) layout = swidgets.SVBoxLayout(self) layout.setContentsMargins(6, 6, 6, 6) layout.addWidget(sketcher) dbb = QtWidgets.QDialogButtonBox self.buttons = dbb(dbb.Cancel) self.buttons.addButton('Use This Structure', dbb.ApplyRole) self.buttons.clicked.connect(self.useStructure) self.buttons.rejected.connect(self.reject) layout.addWidget(self.buttons) size = self.size() size.setHeight(700) size.setWidth(700) self.resize(size)
[docs] def useStructure(self, button): """ Prompt the EndGroupRow to use the currently sketched structure, and close the dialog if the structure is acceptable """ if self.buttons.buttonRole(button) != self.buttons.ApplyRole: return success = self.master.useCustomTemplate() if success: return self.accept() return None
[docs]class TMLigandRowMixin(object): """ A mixin class that takes care of minimizing bidentate transition metal complex ligand structures so that they remain planar through the R1-...-R2 bond path. Should be used with ItemRow classes. Example use: class LigandRow(TMLigandRowMixin, ItemRow): ... """
[docs] def findAttachmentMarkers(self, set_dentation=True): """ Find the atoms that mark attachment points and determine the mono/bi-dentation of the ligand. The index of each marker atom is stored in self.markers. :type set_dentation: bool :param set_dentation: Whether to set the dentation_type property based on the current number of marker atoms of the structure """ # Call the parent class method first. super(TMLigandRowMixin, self).findAttachmentMarkers() if set_dentation: if len(self.markers) == 2: self.dentation_type = buildcomplex.BIDENTATE elif len(self.markers) == 1: self.dentation_type = buildcomplex.MONODENTATE
[docs] def prepareStructureForMinimization(self): """ Prepare the structure for minimization. We change any eta-coordination marker to a dummy atom because that marker is not an actual atomic position - we don't want it influencing the location of other atoms. For monodentate ligands: If not eta-coordination, the location of the first marker is important because that determines the ligand-metal bond vector. We minimize this position by placing an "H" at that location. For bidentate ligands: If neither marker is eta, we change the second marker to a dummy atom in order to avoid both marker atoms clashing sterically - in the complex there will be only the metal atom there so setting the second marker to a dummy ensures that the metal is in a good bidentate binding pocket. If one or both markers are eta, we leave any remaining non-eta marker as an H atom to optimize its position. The location of any eta-marker is updated afterwards based on the position of the minimized eta-coordinating atoms. """ firstm = self.markers[0] firstm_is_eta = firstm in self.eta_indexes if firstm_is_eta: # Don't let eta-coordination marker affect other atoms element = 'DU' else: element = 'H' buildcomplex.transmute_atom(self.structure.atom[firstm], element) if self.dentation_type == buildcomplex.BIDENTATE: secondm = self.markers[1] if secondm in self.eta_indexes: # Don't let eta-coordination marker affect other atoms element = 'DU' else: if firstm_is_eta: # This is the only non-eta position, optimize it element = 'H' else: # Remove this non-eta position so it doesn't interfere with # the other non-eta position element = 'DU' buildcomplex.transmute_atom(self.structure.atom[secondm], element)
[docs] def addHydrogens(self): """ Add hydrogens to the structure, properly accounting for phantom Rx bonds and incorrect formal charges caused by those bonds. """ # Eta-coordination is denoted in the 2D sketcher by bonding an Rx # atom to multiple atoms. That Rx bond occupies a valence position as # far as the add_bond method is concerned, so we need to break all those # bonds before adding hydrogens and then add them back after eta_neighbors = {} for index in self.eta_indexes: neighbors = list(self.structure.atom[index].bonded_atoms) eta_neighbors[index] = neighbors for neighbor in neighbors: self.structure.deleteBond(index, neighbor.index) # For some reason, N atoms automatically get +1 charge in the # sketcher when R1 violates its normal valence, while other # elements do not if neighbor.element == 'N': order = sum([x.order for x in neighbor.bond]) if order == 3: neighbor.formal_charge = 0 build.add_hydrogens(self.structure) for eta_index, neighbors in eta_neighbors.items(): for neighbor in neighbors: self.structure.addBond(eta_index, neighbor.index, 1)
[docs] def modifyMinimizer(self, mizer): """ Add torsion constraints to the minimizer to keep the coordination sphere planar and with torsions of 0. :type mizer: schrodinger.structutils.minimizer.Minimizer :param mizer: The Minimizer object that will minimize the structure. """ struct = mizer.getStructure() large_force = 10000. zero_torsion = 0.0 # Make sure eta-complexed atoms remain planar for index in self.eta_indexes: eta_atoms = set(struct.atom[index].bonded_atoms) for tor in analyze.torsion_iterator(struct, atoms=eta_atoms): mizer.addTorsionRestraint(tor[0], tor[1], tor[2], tor[3], large_force, zero_torsion) # Make sure bidentate coordination sphere remain planar if self.dentation_type == buildcomplex.BIDENTATE: index1 = self.markers[0] index2 = self.markers[1] if (index1 not in self.eta_indexes and index2 not in self.eta_indexes): bond_path = analyze.find_shortest_bond_path( struct, index1, index2) if len(bond_path) < 4: # The ligand part of the coordination ring is too short # for a torsion return for start in range(0, len(bond_path) - 3): inds = bond_path[start:start + 4] mizer.addTorsionRestraint(inds[0], inds[1], inds[2], inds[3], large_force, zero_torsion)
[docs] def postTreatMinimizedStructure(self, marker_element=TEMP_ELEMENT): """ Undo the modifications to the structure that we made before minimization. This means we add back the second marker and point it at the first marker. The location of the second marker actually doesn't really matter, but pointed at the first marker is as good a place as any. """ # Turn the marker atoms back to the Temp Element for index in self.markers: buildcomplex.transmute_atom(self.structure.atom[index], marker_element) # Because eta marker position is determined by the centroid of the eta # atoms, relocate it now that the eta atoms have been minimized self.relocateEtaMarkers()
[docs] def relocateEtaMarkers(self): """ Move each eta marker to the centroid of the atoms it marks """ for index in self.eta_indexes: eta_atom = self.structure.atom[index] bonded = [x.index for x in eta_atom.bonded_atoms] eta_atom.xyz = transform.get_centroid(self.structure, atom_list=bonded)[:3]
[docs]class ItemRowWithTemplates(ItemRow): """ An ItemRow that holds the data for one unit and includes a combo for choosing Template structures """ dataChanged = QtCore.pyqtSignal()
[docs] def __init__(self, master, row_layout, item_type, template_selector_class, default_template, stretch=True, unset_tip=None, builtin_dir=None, sketcher=True, sketcher_class=None, name_field=True, none_item=False, single_rx_atom=False): """ Create a EndGroupRow instance :type master: `schrodinger.ui.qt.appframework.AppFramework` :param master: The parent panel for this row. Must have the following methods: deleteRow, setWaitCursor, restoreCursor :type row_layout: QLayout :param row_layout: The layout this EndGroupRow should add itself to :type item_type: str :param item_type: A string describing the type of item this row is, such as "monomer" - this will be displayed to the user in labels and tooltips. Use the lowercase form of the word - it will be capitalized when necessary. :type template_selector_class: type(TemplateSelector) :param template_selector_class: A fully-implemented subclass of TemplateSelector (the class, not an instance) :type stretch: bool :param stretch: Should a layout stretch be added after all widgets have been laid out :type default_template: str :param default_template: The default template to load for this row when created. :type unset_tip: str :param unset_tip: The tooltip to use when no structure has been set :type builtin_dir: str :param builtin_dir: The path to the directory where built-in templates are stored. By default, it will be the script's '_dir' directory + the template name :type sketcher: bool :param sketcher: Whether to use a button that opens a sketcher window :type sketcher_class: `SketcherBox` :param sketcher_class: The 2D sketcher class to use in the Sketcher dialog window :type name_field: bool :param name_field: True if a name field should be added, False if not :type none_item: bool :param none_item: Combox has a None item if True :param single_rx_atom: If True, there can only be one single rx atom :type single_rx_atom: bool """ self.none_item = none_item self.single_rx_atom = single_rx_atom if unset_tip is None: unset_tip = ('Choose a structure from the templates') ItemRow.__init__(self, master, row_layout, item_type, unset_tip) self.mol_weight = 0.0 self.num_atoms = 0 self.template_dirname = item_type + '_templates' if builtin_dir is None: self.built_in_path = self.template_dirname else: self.built_in_path = builtin_dir self.item_type = item_type # We need a sketcher instance even if it isn't shown, because that is # what handles templates if not sketcher_class: self.sketcher_class = SketcherBox else: self.sketcher_class = sketcher_class self.createSketcher() if sketcher: self.sketch_btn = swidgets.SPushButton('Sketch...', command=self.sketchStructure, layout=self.mylayout) self.template_selector = template_selector_class( layout=self.mylayout, command=self.setStructure, default_selection=default_template) # Item name self.mylayout.addWidget(self.status_label) if name_field: self.name_edit = swidgets.SLabeledEdit('Name:', stretch=False, layout=self.mylayout) # Policy expands upon resizing the parent panel (MATSCI-11028) self.name_edit.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) else: self.name_edit = None if stretch: self.mylayout.addStretch(1000) # Add the default template to the sketcher's list of templates self.default_template = default_template templates = self.sketcher.templates if self.default_template not in templates: templates.insert(0, self.default_template) self.updateTemplates(templates, self.default_template) self.sketcher.templatesUpdated.connect(self.updateTemplates)
[docs] def reset(self): """ Reset all widgets """ ItemRow.reset(self) if self.name_edit: self.name_edit.setEnabled(True) self.name_edit.reset() self.updateTemplates(self.sketcher.templates, self.default_template)
[docs] def setSingleRxAtom(self, single_rx_atom): """ Set the single Rx atom mode. :param single_rx_atom: If True, there can only be one single rx atom :type single_rx_atom: bool """ self.single_rx_atom = single_rx_atom self.sketcher.single_rx_atom = self.single_rx_atom self.filterTemplates()
[docs] def filterTemplates(self): """ Filter templates based on whether single rx atom is requested. """ templates = self.sketcher.filterTemplates() if templates: self.template_selector.silentlySetTemplates(templates)
[docs] def useCustomTemplate(self): """ Set the row to use the Custom template option """ self.template_selector.silentlyTryToSelectTemplate(CUSTOM_TEMPLATE) self.setStructure() return bool(self.structure)
[docs] def sketchStructure(self): """ Open up a sketcher to allow the user to sketch a new structure """ title = self.item_type.replace('_', ' ').title() + ' Sketcher' sketch_dialog = SketchDialog(self, self.sketcher, title) # Load the structure in the 2D sketcher if self.structure is not None: rx_atoms, max_x = get_marker_atom_indexes_from_structure( self.structure) self.sketcher.setSketcherStructure(self.structure) # Change label of from Temp_element to Rx number self.sketcher.setAtomRNumbers(self.structure, rx_atoms) sketch_dialog.exec() # Create a new sketcher as the old one gets deleted by the Dialog self.createSketcher() # User may have saved a new template even if they canceled the dialog self.reloadTemplateList()
[docs] def reloadTemplateList(self, select=""): """ Reload the template selector with current information. If the current selection is not in the currect list of templates, then fall back to the default. :type select: str :param select: The template to select after the list is re-populated. May be the constant RESELECT_CURRENT_TEMPLATE to pick whatever the current template is """ self.sketcher.buildTemplateList(select=select) selection = str(self.template_selector.getSelection()) if selection not in self.sketcher.templates: selection = self.default_template self.updateTemplates(self.sketcher.templates, selection) self.filterTemplates()
[docs] def getNumMarkers(self): """ Get the number of Rx atoms in this row :rtype: int :return: The number of Rx atoms in this row """ return len(self.markers)
[docs] def getAtomsAndWeight(self): """ Count the number of atoms and atomic weight for this row :rtype: (int, float) :return: The number of atoms and total molecular weight for the structure of this row - does not include marker atoms """ if not self.structure: return 0, 0.0 scopy = self.structure.copy() scopy.deleteAtoms(self.markers) return scopy.atom_total, scopy.total_weight
[docs] def setStructure(self): """ Set the structure for this row. """ selection = self.template_selector.getSelection() if selection == NONE_ITEM: self.structure = None self.num_atoms = self.mol_weight = 0 self.markers = [] self.eta_indexes = set() self.status_label.unset() self.name_edit.clear() self.name_edit.setEnabled(False) return self.name_edit.setEnabled(True) # Don't post a warning if this is called as a result of the template # combo being set to Custom and the sketcher is empty self.quiet_if_empty = selection == CUSTOM_TEMPLATE super().setStructure() self.quiet_if_empty = False if self.structure: self.num_atoms, self.mol_weight = self.getAtomsAndWeight() if self.name_edit: self.name_edit.setText(self.structure.title) else: self.num_atoms = self.mol_weight = 0 if self.name_edit: self.name_edit.clear() self.dataChanged.emit()
[docs] def getStructure(self): """ Get the structure either from the sketcher or from a template file """ template_name = str(self.template_selector.getSelection()) if template_name == CUSTOM_TEMPLATE: self.structure = self.sketcher.getSketcherStructure( quiet_if_empty=self.quiet_if_empty) else: self.structure = self.sketcher.readTemplateStructure(template_name)
[docs] def updateTemplates(self, templates, select): """ Load a new list of templates into the template list :type templates: list :param templates: list of template names :type select: str :param select: Attempt to select this template in the combo after loading new names """ if select == RESELECT_CURRENT_TEMPLATE: select = self.template_selector.getSelection() if self.none_item and NONE_ITEM not in templates: templates += [NONE_ITEM] self.template_selector.silentlySetTemplates(templates) self.template_selector.silentlyTryToSelectTemplate(select) if str(self.template_selector.getSelection()) != CUSTOM_TEMPLATE: self.setStructure()
[docs] def getName(self, no_h=False): """ Get the name the user has specified for this item :type no_h: bool :param no_h: If True, return an empty string if the name is "H" :rtype: str :return: The name for this row """ if not self.name_edit: return "" name = str(self.name_edit.text()) if no_h and name == 'H': name = "" return name
[docs] def getASLName(self): """ Modify the user's name to make it acceptable for ASL syntax, which reserves some characters. :rtype: str :return: The name for this row modified to pass ASL syntax """ asl_name = self.getName() for special in [' ', ',', '-', '\t', '>', '<', '=', '*', '?', '#']: asl_name = asl_name.replace(special, "") return asl_name
[docs] def hasStructure(self): """ Is a structure set for this row? :rtype: bool :return: True if yes, False if no """ return bool(self.structure)
[docs] def createSketcher(self): """ Create a new Sketcher for this row """ self.sketcher = self.sketcher_class(self.master, self.built_in_path, self.template_dirname, None, single_rx_atom=self.single_rx_atom)
[docs]class ComplexLigandRxMixin(object):
[docs] def validateRAtomIdentity(self, rx_atoms, max_x): """ Run validation on the values of the R atoms. An error message is displayed to the user by this method if appropriate :type rx_atoms: dict :param rx_atoms: keys are the int value of x in Rx, values are lists of atom indexes set to that Rx value (atom indexes are 1-based) :type max_x: int :param max_x: The larget value of x in the keys of rx_atoms. Unused, kept for API compatibility with parent class. :rtype: bool :return: True if everything is OK, False if not """ msg = ('Bond coordination sites must be marked with a subsituent ' 'labeled R1 for the first coordination site and R2 for the ' 'second coordination site (if applicable). Both covalent bonding' ' and dative bonding sites should be marked by an Rx subsituent.' ' %s-coordination should be designated by bonding the ' 'appropriate Rx site to all %s-coordinating atoms for at site. ' 'No value of R other than 1 or 2 should be used, and no value ' 'should be used more than once. %s' % (swidgets.GL_eta, swidgets.GL_eta, MARKING_INFO)) valid = True r1_atoms = len(rx_atoms.get(1, [])) r2_atoms = len(rx_atoms.get(2, [])) allowed = {1, 2} if not rx_atoms: # Nothing marked valid = False elif any(True for x in (r1_atoms, r2_atoms) if x > 1): # Can't have more than one of the same Rx valid = False elif not r1_atoms: # Must have at least one of R1 valid = False elif any(x for x in rx_atoms.keys() if x not in allowed): # Only 1 and 2 are allowed x values valid = False if not valid: self.warning(msg) return valid
[docs] def validateRAtomStructure(self, struct, rx_atoms, max_x): """ Run validation on the R atoms that requires a `schrodinger.structure.Structure` object. :type struct: `schrodinger.structure.Structure` :param struct: The structure to use for validating the Ra atoms :type rx_atoms: dict :param rx_atoms: keys are the int value of x in Rx, values are lists of atom indexes set to that Rx value (atom indexes are 1-based) :type max_x: int :param max_x: The larget value of x in the keys of rx_atoms. Unused, kept for API compatibility with parent class. :rtype: bool :return: True if everything is OK, False if not """ eta_indexes = get_eta_marker_indexes(struct) markers = [] for xval, indexes in rx_atoms.items(): markers.extend(indexes) # Check for the same atom neighbor for 2 Rx atoms all_atoms = set() for index in markers: for atom in struct.atom[index].bonded_atoms: if atom.index in all_atoms: self.warning('Multiple R1,2 atoms may not be bonded to the ' 'same atom') return False all_atoms.add(atom.index) # Check to ensure that ETA markers don't break up actual bonding # patterns for index in eta_indexes: scopy = struct.copy() scopy.deleteAtoms([index]) if scopy.mol_total > 1: self.warning('Rx markers for eta-coordination can only be ' 'bonded to atoms that are connected to each other') return False return True
[docs]class ComplexTemplateSketcher(ComplexLigandRxMixin, SketcherBox): """ A 2D sketcher that is decorated with a number of widgets for creating and saving templates. Overrides the parent class mainly for validation of the template structure. This class is specifically for templates for the Single and Multi complex builders, which use a different directory name for historical reasons """
[docs] def __init__(self, *args, **kwargs): args = list(args) args[2] = 'complex_templates' SketcherBox.__init__(self, *args, **kwargs) self.moveOldTemplates()
[docs] def moveOldTemplates(self): """ Move any templates from the old location to the new one that is used starting in 2014-3 """ old_dir = os.path.join(self.custom_path, os.pardir, os.pardir, 'complex_templates') if os.path.exists(old_dir): for filename in glob.iglob(old_dir + '/*'): filebase = os.path.basename(filename) new_path = os.path.join(self.custom_path, filebase) if os.path.exists(new_path): fileutils.force_remove(filename) else: fileutils.force_rename(filename, new_path) fileutils.force_rmtree(old_dir) return True
[docs]class LigandSketcherStructGetter(ComplexLigandRxMixin, TMLigandRowMixin, MinimizeMixin, SketcherStructureMixin): """ Gets a structure from the sketcher, marks the attachment points, checks for valid attachment points and creates a 3D structure. """
[docs] def __init__(self, sketcher, master): """ Create a LigandSketcherStructGetter instance :type sketcher: `schrodinger.ui.sketcher.sketcher` :param sketcher: The sketcher instance :type master: QWidget :param master: A widget with a warning method """ self.master = master self.warning = master.warning self.sketcher = sketcher
[docs] def getStructure(self): """ Get the structure from the sketcher, validate it, and convert it to 3D :note: Note that various methods called from here will post warning dialogs using the master widget (passed into the __init__ method) warning method when things go wrong. :rtype: `schrodinger.structure.Structure` or None :return: The 3D structure or None if an error occured. """ self.structure = self.getSketcherStructure() if not self.structure: return None self.findAttachmentMarkers() # Do this before adding hydrogens because it converts marker atoms to H # or Dummies so we don't add H to them. self.prepareStructureForMinimization() # The sketcher structure doesn't include hydrogen atoms by default self.addHydrogens() self.convert2DTo3DAndMinimize() self.postTreatMinimizedStructure(marker_element='Du') return self.structure