__doc__ == """
Functions to control picking of atoms and bonds.
This module contains classes to support controls for picking of atoms
and bonds for scripts that are running inside of Maestro. The main
classes are PickAtomToggle, PickBondToggle and PickTorsionToggle. These are
created with an instances of a QCheckBox and will handle everything that is
needed to support the use of that toggle button for picking atoms (or bonds).
Copyright Schrodinger, LLC. All rights reserved.
"""
import inspect
import numpy
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.structutils import analyze
from schrodinger.structutils import measure
from schrodinger.ui.picking_dir import \
pick_constraint_res_no_maestro_dialog_ui # noqa
from schrodinger.ui.picking_dir import pick_lig_no_maestro_dialog_ui
from schrodinger.ui.picking_dir import pick_res_no_maestro_dialog_ui
from schrodinger.ui.qt import table_helper
from schrodinger.ui.qt.appframework2.markers import MarkerMixin
# Check for Maestro
try:
# Import schrodinger.maestro.maestro directly in order to use private
# functions.
from schrodinger.maestro import maestro
from schrodinger.maestro import markers
except ImportError:
maestro = None # for pychecker
markers = None
EMPTY_ASL = 'not all'
class _PickToggle(object):
"""
Base class for picking toggles. This class should be not used
directly, use the derived classes PickAtomToggle or PickBondToggle
instead
The following options are supported:
command - will be run when button is checked or unchecked
"""
def __init__(self, checkbox, command, pick_text):
self._checkbox = checkbox
self._user_command = command
self._pick_text = pick_text
# Connect up our slot
self._checkbox.stateChanged.connect(self.fullCommand)
self.turning_on = False
self.picked = None
def fullCommand(self, state):
"""
Gets called when the checkbutton is toggled. Manages settings of all
pick toggles, and then calls the user-specified command (if it exists).
"""
if state == QtCore.Qt.Checked and maestro:
# We are turning on now, but
# if we were the last picker to have Maestro's picking rights, we'll
# get notified to turn 'off' by the picking loss callback (even if
# we'd previously called picking_stop(). Make sure we don't turn
# ourselves off.
self.turning_on = True
# Notify any other pick toggles that they just lost picking rights
maestro.invoke_picking_loss_callback()
# Now set ourselves up to be notified if we lose picking rights
maestro.picking_loss_notify(self.stop)
# Ev:114990 This statement must be above the call to
# self._user_command()
self.turning_on = False
if self._user_command:
self._user_command()
def on(self):
return self._checkbox.isChecked()
def start(self):
self._checkbox.setChecked(True)
# setChecked won't trigger the 'clicked()' signal
self.fullCommand(QtCore.Qt.Checked)
def stop(self):
if not self.turning_on:
self._checkbox.setChecked(False)
# setChecked won't trigger the 'clicked()' signal
self.fullCommand(QtCore.Qt.Unchecked)
[docs]class PickAtomToggle(_PickToggle):
"""
Class meant to replicate Maestro atom pick toggles.
Takes an argument 'checkbox' that represents the checkbox for the
picking toggle.
"""
[docs] def __init__(
self,
checkbox,
pick_function,
pick_text="Pick an atom",
enable_lasso=False,
):
"""
The following options are supported:
:type checkbox: QCheckBox instance.
:param checkbox: Checkbox to hook up the class to.
:type pick_function: callable
:param pick_function: will be called when an atom is picked. Must be
a callable function that accepts one argument (atom number, or
ASL, if enable_lasso is True).
:type pick_text: str
:param pick_text: Text that will be displayed in Maestro's status area
(default "Pick an atom").
:type enable_lasso: bool
:param enable_lasso: Whether to allow multiple atoms to be selected
simultaneously via lasso.
"""
self._pick_function = pick_function
if not callable(self._pick_function):
raise SyntaxError('PickAtomToggle: "pick_function" must be '
'callable')
self.enable_lasso = enable_lasso
_PickToggle.__init__(self, checkbox, self._command, pick_text)
def _command(self):
""" Gets called when checkbutton is checked or unchecked """
if maestro:
if self.on(): # just got toggled on:
if self.enable_lasso:
maestro.picking_lasso_start(self._pick_text,
self._pick_function)
else:
maestro.picking_atom_start(self._pick_text,
self._pick_function)
else: # just got toggled off:
maestro.picking_stop()
[docs]class PickBondToggle(_PickToggle):
"""
Class meant to replicate Maestro bond pick toggles.
The argument 'checkbox' represents the QCheckBox object for the
picking toggle
The following options are supported:
pick_function - will be called when a bond is picked. Must be a callable
function that accepts two arguments (atom numbers)
pick_text - text that will be displayed in Maestro's status area
(default "Pick a bond")
"""
[docs] def __init__(self, checkbox, pick_function, pick_text="Pick a bond"):
self._pick_function = pick_function
if not callable(self._pick_function):
raise SyntaxError('PickBondToggle: "pick_function" must be '
'callable')
_PickToggle.__init__(self, checkbox, self._command, pick_text)
def _command(self):
""" Gets called when checkbutton is checked or unchecked """
if maestro:
if self.on(): # just got toggled on:
maestro.picking_bond_start(self._pick_text, self._pick_function)
else: # just got toggled off:
maestro.picking_stop()
[docs]class PickMixedToggle(_PickToggle):
"""
Class allowing to pick atom or bond depending on internal state.
"""
[docs] def __init__(self,
checkbox,
pick_atom_function,
pick_bond_function,
pick_atom_text="Pick an atom",
pick_bond_text="Pick a bond",
enable_lasso=False):
"""
Initialize picker class.
:param checkbox: pick toggle
:type checkbox: `QtWidgets.QCheckBox`
:param pick_atom_function: this function is called when atom is picked.
Must be a callable function that accepts one
arguments (atom number, or ASL if
enable_lasso is True).
:type pick_atom_function: function
:param pick_bond_function: this function is called when bond is picked.
Must be a callable function that accepts two
arguments (bond atoms).
:type param_bond_function: function
:param pick_atom_text: atom pick text that will be displayed in
Maestro's status area (default "Pick an atom").
:type pick_atom_text: str
:param pick_bond_text: bond pick text that will be displayed in
Maestro's status area (default "Pick an atom").
:type pick_bond_text: str
:param enable_lasso: Whether to allow multiple atoms to be selected
simultaneously via lasso.
:type enable_lasso: param
"""
self._pick_atom_function = pick_atom_function
self._pick_bond_function = pick_bond_function
self._pick_atom_text = pick_atom_text
self._pick_bond_text = pick_bond_text
for func in (self._pick_atom_function, self._pick_bond_function):
if not callable(func):
raise SyntaxError('PickBondToggle: "pick_function" must be '
'callable')
# We start in 'pick atom' state.
self.pick_atom = True
self._enable_lasso = enable_lasso
self._pick_function = pick_atom_function
pick_text = self._pick_atom_text
_PickToggle.__init__(self, checkbox, self._command, pick_text)
# Make it possble to toggle enable_lasso after the class was instantiated:
def _getEnableLasso(self):
return self._enable_lasso
def _setEnableLasso(self, new_value):
self._enable_lasso = new_value
if self.on() and self.pick_atom:
self.stop()
self.start()
enable_lasso = property(_getEnableLasso, _setEnableLasso)
def _command(self):
""" Gets called when checkbutton is checked or unchecked """
if maestro:
if self.on(): # just got toggled on:
if self.pick_atom:
if self.enable_lasso:
maestro.picking_lasso_start(self._pick_text,
self._pick_function)
else:
maestro.picking_atom_start(self._pick_text,
self._pick_function)
else:
maestro.picking_bond_start(self._pick_text,
self._pick_function)
else: # just got toggled off:
maestro.picking_stop()
[docs] def setPickAtom(self, state):
"""
Turn pick atom mode on and off. If it's off bonds will be picked
instead.
:param state: True or False, if True atoms will be picked. Otherwise
bonds will be picked.
:type state: bool
"""
self.pick_atom = state
self.stop()
if state:
self._pick_text = self._pick_atom_text
self._pick_function = self._pick_atom_function
else:
self._pick_text = self._pick_bond_text
self._pick_function = self._pick_bond_function
self.start()
[docs] def setPickBond(self, state):
"""
Convenience function to turn pick atom mode on and off. If its off
bonds will be picked instead.
:param state: True or False, if True bonds will be picked. Otherwise
atoms will be picked.
:type state: bool
"""
self.setPickAtom(not state)
[docs]class PickCategoryToggle(_PickToggle):
"""
Class to pick graphics objects using pick categories
"""
[docs] def __init__(self, checkbox, pick_function, pick_category):
"""
:param checkbox: The checkbox instance to control picking
:type checkbox: QtWidgets.QCheckBox
:param pick_function: Module-level function that takes one argument
(the `pick_id` attribute of the picked graphics object). Must be a
module-level function because maestro will call it by name, rather
than by reference.
:type pick_function: callable
:param pick_category: The `pick_category` attribute of the graphics
objects to pick. Must be defined in mm_graphicspick.cxx
:type pick_category: str
"""
if not callable(pick_function):
raise ValueError(
f"pick_function must be callable, not {type(pick_function)}")
if inspect.ismethod(pick_function):
raise ValueError(
"pick_function must be a module-level function, not a method")
self._pick_function = pick_function
self._pick_category = pick_category
# Category picking does not use pick text but it's a required argument
# in the superclass
_pick_text = ""
super().__init__(checkbox, self._command, _pick_text)
def _command(self):
if maestro:
if self.on():
maestro.add_pick_category_callback(self._pick_function,
self._pick_category)
else:
maestro.remove_pick_category_callback(self._pick_category)
# Get rid of the picking rights
maestro.picking_stop()
class _PickGroupToggleBase(_PickToggle):
"""
Class for creating a picker that allows the user to define a group
of atoms of a certain given size by sequentially picking atoms. Takes
an argument 'checkbox' that represents the checkbox for the picking
toggle.
"""
def __init__(self,
checkbox,
pick_function,
pick_text="Pick atoms to define a group",
allow_locked_entries=False):
"""
:type checkbox: `QtWidgets.QCheckBox`
:param checkbox: Checkbox widget to convert into a pick toggle.
:type pick_function: function
:param pick_function: This function is called when atoms are selected in
the workspace. Must be a callable that accepts an ASL string
:type pick_text: str
:param pick_text: Text to display in Maestro's status area while the
toggle is checked (default: "Pick atoms to define a group").
:param allow_locked_entries: Whether to allow picking in locked entries
:type allow_locked_entries: bool
:raises TypeError: If the pick_function argument is not a callable
"""
if not callable(pick_function):
raise TypeError('"pick_function" must be callable')
super().__init__(checkbox, self._command, pick_text)
self._pick_function = pick_function
self._allow_locked_entries = allow_locked_entries
self.current_selection = None
self._reset_selection()
self._marker = None
if maestro:
self._marker = markers.Marker(color=(1.0, 0.8, 0.8)) # Pink
maestro.workspace_changed_function_add(self._workspaceChanged)
def _reset_selection(self):
"""
Subclasses should define this method to so that it resets current
selection to a predetermined default. e.g. `self.current_selection = []`
"""
raise NotImplementedError()
def reset(self):
"""
Unpick all atoms and update markers
"""
self._reset_selection()
if self.on():
self.callPickFunction()
self._updateMarkers()
def callPickFunction(self):
"""
Call the pick function with the current selection
"""
self._pick_function(self.current_selection)
def _command(self):
"""
Called when checkbutton is checked or unchecked
"""
if maestro:
if self.on(): # just got toggled on:
self._startPicking()
else: # just got toggled off:
maestro.picking_stop()
self._updateMarkers()
def _startPicking(self):
"""
Subclasses should define this as a function that will start maestro
picking using one of the functions in maestro.py
"""
raise NotImplementedError()
def _workspaceChanged(self, changed):
"""
Called when the Workspace changed event is received.
"""
if changed in (maestro.WORKSPACE_CHANGED_EVERYTHING,
maestro.WORKSPACE_CHANGED_CONNECTIVITY):
self.reset()
def _updateMarkers(self):
"""
Update markers to reflect current picker selection
"""
if not markers or not maestro:
return
self._marker.show(asl=self._getMarkerAsl())
def _getMarkerAsl(self):
"""
Get ASL that should be shown in markers. Must be implemented in
subclass to return a valid ASL string
"""
raise NotImplementedError()
def setPickText(self, new_text):
"""
Set's the text that is shown in the workspace banner
:param new_text: The text to show in the banner
:type new_text: str
"""
self._pick_text = new_text
if self.on():
self.stop()
self.start()
[docs]class PickAslToggle(_PickGroupToggleBase):
"""
The pick toggle makes use of maestro's picking_asl_start which allows a
custom pick state to be chosen (residue, molecule, chain, etc.) These
pick states are defined as constants in maestro.py. The pick function
that is passed as an argument must take a string as an
argument, this string will be a valid asl statement
"""
[docs] def __init__(self,
checkbox,
pick_function,
picking_mode,
pick_text="Pick atoms to define a group",
allow_locked_entries=False):
"""
See parent class for argument documentation.
:type picking_mode: int
:param picking_mode: The mode to set Maestro's picker feature to.
These are defined in schrodinger.maestro.maestro
"""
super().__init__(checkbox, pick_function, pick_text,
allow_locked_entries)
self.picking_mode = picking_mode
def _reset_selection(self):
self.current_selection = EMPTY_ASL
def _getMarkerAsl(self):
"""
Get the ASL of picked atoms to show as marker
:returns: The ASL of this picker
:rtype: str
"""
if self.on():
return self.current_selection
else:
return EMPTY_ASL
def _onAslPicked(self, asl):
"""
Gets called when something is picked in the workspace
:type asl: str
:param asl: asl of picked atoms
"""
self.current_selection = asl
self.callPickFunction()
self._updateMarkers()
def _startPicking(self):
maestro.picking_asl_start(self._pick_text, self.picking_mode,
self._onAslPicked, self._allow_locked_entries)
[docs]class PickAtomsToggle(_PickGroupToggleBase):
"""
This pick toggle allows you to select multiple atoms at once,
each individually. Clicking on an already selected atom will unselect it. The
pick_function should expect a list of integers (which correspond to
atom indices) as an argument
"""
[docs] def __init__(self,
checkbox,
natoms,
pick_function,
pick_text="Pick atoms to define a group",
allow_locked_entries=False):
"""
See parent class for argument documentation.
:type natoms: int or None
:param natoms: the number of atoms in the group or None to allow any
number of picks
"""
super().__init__(checkbox, pick_function, pick_text,
allow_locked_entries)
self._ct = None
self._natoms = natoms
def _reset_selection(self):
self.current_selection = []
@property
def workspace_ct(self):
"""
Get a copy of the workspace structure
(lazily and cached)
"""
if not maestro:
return
if self._ct is None:
self._ct = maestro.workspace_get()
return self._ct
[docs] def reset(self):
super().reset()
self._ct = None
def _atomPicked(self, anum):
"""
Gets called when an atom is picked.
:type anum: int
:param anum: atom index of picked atom
"""
if anum in self.current_selection:
self._unpickAtom(anum)
else:
# FIXME make sure the user didn't pick an invalid atom, etc.
# This logic is to be added later.
self._pickAtom(anum)
if len(self.current_selection) == self._natoms:
self.callPickFunction()
self.current_selection = []
elif self._natoms is None:
self.callPickFunction()
self._updateMarkers()
def _unpickAtom(self, anum):
self.current_selection.remove(anum)
def _pickAtom(self, anum):
self.current_selection.append(anum)
def _getMarkerAsl(self):
"""
Get the ASL of picked atoms to show as marker
:returns: ASL
:rtype: str
"""
if len(self.current_selection) and self.on():
return "atom.num %s" % ",".join(map(str, self.current_selection))
else:
return EMPTY_ASL
def _startPicking(self):
maestro.picking_atom_start(self._pick_text, self._atomPicked,
self._allow_locked_entries)
# Start from scratch:
self._reset_selection()
[docs]class PickAtomsLassoToggle(PickAtomsToggle):
"""
Class for creating a PickAtomsToggle that allows the user to pick atoms using
marquee selection
"""
[docs] def __init__(self,
checkbox,
pick_function,
pick_text="Pick atoms to define a group",
allow_locked_entries=False):
# See parent class for additional argument documentation.
natoms = None
super().__init__(checkbox, natoms, pick_function, pick_text,
allow_locked_entries)
def _lassoPicked(self, asl):
"""
When atoms are picked in the workspace using the lasso, select newly
picked atoms and deselect atoms which were already picked
:param asl: The asl of the lassoed atoms
:type asl: str
"""
cur_picked = set(self.current_selection)
new = set(analyze.evaluate_asl(self.workspace_ct, asl))
# We deselect the intersection if new and currently picked because
# clicking on a picked atom should deselect it
self.current_selection = list(
cur_picked.union(new) - cur_picked.intersection(new))
self.callPickFunction()
self._updateMarkers()
def _startPicking(self):
maestro.picking_lasso_start(self._pick_text, self._lassoPicked,
self._allow_locked_entries)
self._reset_selection()
[docs]class PickResiduesToggle(PickAtomsToggle):
"""
Class for creating a PickAtomsToggle that allows the user to pick and unpick
residues by clicking on any of their atoms
"""
[docs] def __init__(self,
checkbox,
pick_function,
pick_text="Pick residues",
allow_locked_entries=False):
# See parent class for argument documentation
natoms = None
super().__init__(checkbox, natoms, pick_function, pick_text,
allow_locked_entries)
def _unpickAtom(self, anum):
"""
Unpick the atom and all atoms in the same residue
"""
all_atoms = self._getAllAtomsInRes(anum)
for anum in all_atoms:
self.current_selection.remove(anum)
def _pickAtom(self, anum):
"""
Pick the atom and all atoms in the same residue
"""
all_atoms = self._getAllAtomsInRes(anum)
self.current_selection.extend(all_atoms)
def _getAllAtomsInRes(self, anum):
atom = self.workspace_ct.atom[anum]
residue = atom.getResidue()
all_atoms = residue.getAtomIndices()
return all_atoms
[docs]class PickPairToggle(PickAtomsToggle):
"""
Class for creating a picker that allows the user to define a pair
of atoms by sequentially picking 2 atoms.
"""
[docs] def __init__(self,
checkbox,
pick_function,
pick_text="Pick 2 atoms to define a pair",
allow_locked_entries=False):
# See parent class for argument documentation
natoms = 2
super().__init__(checkbox, natoms, pick_function, pick_text,
allow_locked_entries)
[docs]class PickTorsionToggle(PickAtomsToggle):
"""
Class for creating a picker that allows the user to define a torsion by
sequentually picking 4 atoms.
"""
[docs] def __init__(self,
checkbox,
pick_function,
pick_text="Pick 4 atoms to define a torsion",
allow_locked_entries=False):
# See parent class for argument documentation
natoms = 4
super().__init__(checkbox, natoms, pick_function, pick_text,
allow_locked_entries)
[docs]class Pick3DObjectToggle(_PickToggle):
"""
Class meant to replicate a Maestro pick toggle. This object allows you to
pick objects from the schrodinger.graphics3d module. This picker allows you
to pick any object assigned to the picking category argument 'pick_category'
:param checkbox: The QCheckBox object for the picking toggle
:param pick_function: Will be called when a bond is picked. Must be a
callable function that accepts two arguments (atom
numbers)
:param pick_category: The category of objects to pick. Strings must be
defined in mm_graphicspick.cxx string_pick_map. This will allow picking
of 3D objects with the corresponding pick_category attribute.
:param pick_text: text that will be displayed in Maestro's status area
(default "Pick an object")
"""
[docs] def __init__(self,
checkbox,
pick_function,
pick_category,
pick_text="Pick an object"):
self._pick_function = pick_function
self.pick_category = pick_category
if not callable(self._pick_function):
raise SyntaxError('Pick3DObjectToggle: "pick_function" must be '
'callable')
_PickToggle.__init__(self, checkbox, self._command, pick_text)
def _command(self):
""" Gets called when checkbutton is checked or unchecked """
if maestro:
if self.on(): # just got toggled on:
maestro.start_picking(self._pick_text, self._pick_function,
self.pick_category)
else: # just got toggled off:
maestro.picking_stop()
[docs]class MaestrolessLigandListModel(table_helper.RowBasedListModel):
"""
Model for ligand lists that can be used outside of Maestro.
"""
[docs] def __init__(self, st, parent=None):
"""
:param st: Structure containing ligands
:type st: `schrodinger.structure.Structure`
:param parent: The Qt parent widget.
:type parent: `QtWidgets.QWidget` or None
"""
super(MaestrolessLigandListModel, self).__init__(parent)
self.st = st
finder = analyze.AslLigandSearcher()
self.ligs = finder.search(self.st)
self.replaceRows(self.ligs)
@table_helper.data_method(Qt.DisplayRole)
def _displayData(self, lig):
res_strs = []
for res in lig.st.residue:
res_str = "{0}({1})".format(str(res), res.resnum)
res_strs.append(res_str)
res_str = ", ".join(res_strs)
if res_str.strip() == ":(0)":
# This isn't a useful string; return SMILES instead.
return str(lig)
return res_str
[docs]class MaestrolessPickLigandDialog(QtWidgets.QDialog):
"""
Dialog to allow users to pick ligands outside of Maestro.
Used by ifd_gui.py and covalent_docking_gui.py
"""
# TODO: Move this class into a different module
[docs] def __init__(self, st, parent=None):
"""
:param st: Structure containing the ligands
:type st: `schrodinger.structure.Structure`
:param parent: The dialog's parent widget
:type parent: `QtWidgets.QWidget`
"""
super(MaestrolessPickLigandDialog, self).__init__(parent)
self.ui = pick_lig_no_maestro_dialog_ui.Ui_Dialog()
self.setWindowModality(QtCore.Qt.WindowModal)
self.ui.setupUi(self)
self.picked_ligand = None
self.ligand_list_model = MaestrolessLigandListModel(st, self)
self.ui.ligand_list_view.setModel(self.ligand_list_model)
[docs] def accept(self):
"""
Called when the user clicks OK button
"""
# Only single index selection allowed.
selected_rows = [
index.data(table_helper.ROW_OBJECT_ROLE)
for index in self.ui.ligand_list_view.selectedIndexes()
]
if selected_rows:
# analyze.Ligand object
self.picked_ligand = selected_rows[0]
super(MaestrolessPickLigandDialog, self).accept()
[docs]class ResidueRow(MarkerMixin):
"""
Class representing a residue in the active site. Used by
ResiduesModel and SelectResiduesDialog.
"""
[docs] def __init__(self, res, markers_color=None):
"""
:param res: Residue to be added as a row.
:type res: `schrodinger.structure.Residue`
:param markers_color: Color to add workspace markers as for this residue.
If None, no markers will be added.
:type markers_color: 3-tuple of floats, each between 0.0 and 1.0, or None
"""
super(ResidueRow, self).__init__()
self.short_res_str = str(res)
self.res_str = self.short_res_str
pdbres = res.pdbres.strip()
if pdbres:
self.res_str += " %s" % pdbres
self.mol_num = res.molecule_number
self.atom_coords = [a.xyz for a in res.atom]
if maestro and markers_color:
res_atoms = [atom for atom in res.atom]
self.addMarker(res_atoms, color=markers_color)
[docs] def findInStructure(self, st):
return st.findResidue(self.short_res_str)
[docs]class PickResidueRow(ResidueRow):
"""
Base class for rows to be used in models inheriting
`_BaseMaestrolessPickModel`.
"""
[docs] def __init__(self, res, distance=None):
"""
:param res: Residue for this row
:type res: `schrodinger.structure._Residue`
:param distance: Distance of the residue to a ligand or None
:type distance: float or None
"""
super(PickResidueRow, self).__init__(res)
self.picked = False
self.chain = res.chain
self.pdbres = res.pdbres
self.resnum = res.resnum
self.distance = distance
class _BasePickColumns(table_helper.TableColumns):
Picked = table_helper.Column("", checkable=True)
class _BaseMaestrolessPickModel(table_helper.RowBasedTableModel):
"""
Base model class for tables to enable picking structures outside
of Maestro.
"""
Column = _BasePickColumns
ROW_CLASS = PickResidueRow
def __init__(self, parent, multi_select=True):
self.multi_select = multi_select
super(_BaseMaestrolessPickModel, self).__init__(parent)
@table_helper.data_method(Qt.CheckStateRole)
def _checkStateData(self, col, res_row):
if col == self.Column.Picked:
return Qt.Checked if res_row.picked else Qt.Unchecked
def _setData(self, col, res_row, value, role, row_num):
if col == self.Column.Picked and role == Qt.CheckStateRole:
if self.multi_select:
res_row.picked = bool(value)
else:
for row in self.rows:
row.picked = (row == res_row)
self.columnChanged(self.Column.Picked)
return True
return False
[docs]class PickResiduesChangedMixin(object):
"""
Mixin to provide common signals for dialogs that allow users
to pick residues.
"""
# Tuple of (x, y, z) coordinates.
residues_centroid_changed = QtCore.pyqtSignal(tuple)
# List of selected residue strings (e.g. ['A:217', 'A:312b'])
residues_changed = QtCore.pyqtSignal(list)
[docs] def getResiduesList(self, res_objs):
"""
Return the list of residue strings (e.g. ['A:217', 'A:231b']) of the selected residues.
:param res_objs: List of residue objects to get strings for
:type res_objs: List of `ResidueRow`
:return List of residue strings for each row.
@rtyp: list of str
"""
return [res_obj.res_str.split()[0] for res_obj in res_objs]
[docs] def getResiduesCenter(self, res_objs):
"""
Return the (x, y, z) tuple for the center of the selected residues.
Will raise ValueError if no residues were picked.
@pram res_objs: Residue objects to get the center of
:type res_objs: List of `ResidueRow`
:return: Tuple of center x, y, z coordinates
:rtype: tuple of (float, float, float)
"""
# Calculate the center of the selected residues:
all_coords = []
for res_obj in res_objs:
all_coords += res_obj.atom_coords
if not all_coords:
raise ValueError("No residues specified")
# Will return averages of X, Y, and Z coordinates (as 3-item array):
return tuple(numpy.average(numpy.array(all_coords), 0))
class _BaseMaestrolessPickResDialog(PickResiduesChangedMixin,
QtWidgets.QDialog):
"""
Base class for picking residues outside of Maestro
"""
PickModelClass = _BaseMaestrolessPickModel
def __init__(self,
ui,
st,
lig_st=None,
find_ws_lig=False,
multi_select=True,
parent=None):
"""
:param ui: Module defining the dialog UI.
:type ui: module
:param st: Structure containing the residues
:type st: `schrodinger.structure.Structure`
:param lig_st: Ligand structure to check distances of residues against.
Cannot be specified if find_ws_lig is set to True.
If not specified and find_ws_lig is False, distance filtering will not
be enabled.
:type lig_st: `schrodinger.structure.Structure` or None
:param find_ws_lig: Whether to search self.st for a single ligand. If
more than one ligand is identified, none will be used. Cannot be True if a ligand
is specified via ligand_st.
:type find_ws_lig: bool
:param multi_select: Whether to allow selection of multiple residues,
vs a single residue only. Default is True.
:type multi_select: bool
:param parent: Parent widget to the dialog
:type parent: `QtWidgets.QWidget`
"""
if lig_st and find_ws_lig:
raise ValueError("Cannot specify both ligand_st and find_ws_lig.")
super(_BaseMaestrolessPickResDialog, self).__init__(parent)
self.ui = ui.Ui_Dialog()
self.setWindowModality(QtCore.Qt.WindowModal)
self.ui.setupUi(self)
self.pick_model = self.PickModelClass(self, multi_select)
if not multi_select:
self.ui.choose_res_lbl.setText("Choose residue:")
self.ui.residues_view.setModel(self.pick_model)
self.ui.residues_view.setSelectionBehavior(
QtWidgets.QAbstractItemView.SelectRows)
self.st = st
if find_ws_lig:
finder = analyze.AslLigandSearcher()
ligs = finder.search(self.st)
if len(ligs) == 1:
self.lig_st = ligs[0].st
else:
self.lig_st = None
else:
self.lig_st = lig_st
self.picked_res_rows = []
self.res_rows = []
def getResiduesAndDistances(self):
"""
Return a list of 2-tuples containing
the residues and their distance from a ligand,
if specified.
:return: Tuple of each residue in the structure and the residue distance from
the specified ligand, or None if no ligand is specified.
:rtype: list of (`schrodinger.structure._Residue`, float) or
(`schrodinger.structure._Residue`, None)
"""
res_list = []
for res in self.st.residue:
if self.lig_st:
res_anums = res.getAtomIndices()
res_st = self.st.extract(res_anums)
res_dist = measure.get_shortest_distance(res_st,
st2=self.lig_st)
else:
res_dist = None
res_list.append((res, res_dist))
return res_list
def clear(self):
"""
Unpick all rows in the model.
"""
self.picked_res_rows = []
for row in self.res_rows:
row.picked = False
def accept(self):
"""
Called when the OK button is clicked on the dialog.
"""
self.picked_res_rows = [r for r in self.pick_model.rows if r.picked]
super(_BaseMaestrolessPickResDialog, self).accept()
res_str_list = self.getResiduesList(self.picked_res_rows)
self.residues_changed.emit(res_str_list)
try:
res_center_array = self.getResiduesCenter(self.picked_res_rows)
except ValueError:
# No residues specified
res_center_array = []
self.residues_centroid_changed.emit(res_center_array)
def display(self):
"""
Show the panel
"""
self.exec()
class PickResidueColumns(table_helper.TableColumns):
Picked = table_helper.Column("", checkable=True)
Chain = table_helper.Column("Chain")
Residue = table_helper.Column("Residue")
Resnum = table_helper.Column("No.")
[docs]class PickResidueModel(_BaseMaestrolessPickModel):
"""
Model for tables to allow picking residues from a structure outside of
Maestro.
"""
Column = PickResidueColumns
ROW_CLASS = PickResidueRow
@table_helper.data_method(Qt.DisplayRole)
def _displayData(self, col, res_row):
if col == self.Column.Chain:
return res_row.chain
elif col == self.Column.Residue:
return res_row.pdbres
elif col == self.Column.Resnum:
return res_row.resnum
[docs]class MaestrolessPickResidueDialog(_BaseMaestrolessPickResDialog):
"""
Class for picking residues outside of Maestro.
"""
PickModelClass = PickResidueModel
ALL_RESIDUES = "All residues"
NEAR_RESIDUES = "Residues near ligand (within 5A)"
[docs] def __init__(self,
st,
lig_st=None,
find_ws_lig=False,
multi_select=True,
parent=None):
"""
:param st: Structure containing the residues
:type st: `schrodinger.structure.Structure`
:param lig_st: Ligand structure to check distances of residues against.
Cannot be specified if find_ws_lig is set to True.
If not specified and find_ws_lig is False, distance filtering will not
be enabled.
:type lig_st: `schrodinger.structure.Structure` or None
:param find_ws_lig: Whether to search self.st for a single ligand. If
more than one ligand is identified, none will be used. Cannot be True if a ligand
is specified via ligand_st.
:type find_ws_lig: bool
:param multi_select: Whether to allow selection of multiple residues,
vs a single residue only. Default is True.
:type multi_select: bool
:param parent: Parent widget to the dialog
:type parent: `QtWidgets.QWidget`
"""
super(MaestrolessPickResidueDialog,
self).__init__(ui=pick_res_no_maestro_dialog_ui,
st=st,
lig_st=lig_st,
find_ws_lig=find_ws_lig,
multi_select=multi_select,
parent=parent)
self.pick_model.dataChanged.connect(self.updateNumPickedResiduesLabel)
for res, res_dist in self.getResiduesAndDistances():
res_row = PickResidueRow(res, res_dist)
self.res_rows.append(res_row)
self.ui.show_combo.addItems([self.ALL_RESIDUES, self.NEAR_RESIDUES])
self.ui.show_combo.currentIndexChanged.connect(
self.onShowComboIndexChanged)
self.ui.show_combo.setCurrentIndex(0)
self.onShowComboIndexChanged()
if not self.lig_st:
self.ui.show_combo.setEnabled(False)
[docs] def af2SettingsGetValue(self):
"""
Used with `schrodinger.ui.qt.appframework2.settings.SettingsMixin`
to save the dialog state to JSON.
:return: List of panel attributes to serialize
:rtype: list
"""
pick_list = [row.picked for row in self.res_rows]
return [self.ui.show_combo.currentIndex(), pick_list]
[docs] def af2SettingsSetValue(self, value):
"""
Used with `schrodinger.ui.qt.appframework2.settings.SettingsMixin`
to reload the dialog state from JSON.
:param value: Values to set for the panel
:type value: list
"""
self.ui.show_combo.setCurrentIndex(value[0])
for row, picked in zip(self.res_rows, value[1]):
row.picked = picked
[docs] def onShowComboIndexChanged(self):
"""
Called when the index of the Show combo box is changed. Updates the
available residues based on the selected option.
"""
show_all = self.ui.show_combo.currentText() == self.ALL_RESIDUES
if show_all:
self.pick_model.replaceRows(self.res_rows)
else:
self.pick_model.replaceRows(
[row for row in self.res_rows if row.distance <= 5.0])
self.ui.residues_view.resizeColumnsToContents()
self.updateNumPickedResiduesLabel()
[docs] def updateNumPickedResiduesLabel(self):
"""
Update the label for number of picked residues.
"""
rows = list(self.pick_model.rows)
num_picked = len([row for row in rows if row.picked])
self.ui.res_count_label.setText("{0} total; {1} selected".format(
len(rows), num_picked))