"""
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 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 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 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 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 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 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