Source code for schrodinger.ui.picking

__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))