Source code for schrodinger.application.phase.phase_widgets
from collections import OrderedDict
import schrodinger
from schrodinger import project
from schrodinger.application.phase import constants
from schrodinger.application.phase import hypothesis
from schrodinger.application.phase import pt_hypothesis
from schrodinger.infra import phase
from schrodinger.project import ProjectException
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import delegates as qt_delegates
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import table_helper
from schrodinger.ui.qt.multi_combo_box import MultiComboBox
maestro = schrodinger.get_maestro()
FEATURE_TYPE_ROLE = Qt.UserRole + 1
[docs]class FeatureOptionsCombo(MultiComboBox):
    """
    MultiComboBox containing feature presets. This currently includes feature
    equivalencies and use of alternate feature definitions.
    """
    # Feature equivalencies dictionary whose values are equivalency arguments
    FEATURE_EQUIV_PRESETS = OrderedDict(
        (("Make hydrophobic and aromatic rings equivalent",
          "HR"), ("Make acceptor and negative equivalent", "AN"),
         ("Make donor and positive equivalent", "DP")))
    FEATURE_DEFINITION_PRESETS = [
        "Replace vectors with projected points (acceptors and donors)"
    ]
[docs]    def __init__(self, parent):
        super(FeatureOptionsCombo, self).__init__(parent)
        self.addItems(list(self.FEATURE_EQUIV_PRESETS))
[docs]    def addFeatureDefinitionPresets(self):
        """
        Adds the optional feature definition presets.
        """
        self.addItems(self.FEATURE_DEFINITION_PRESETS)
[docs]    def getSelectedFeatureEquivalencies(self):
        """
        Return a list of feature equivalencies that were checked/selected in
        the menu.
        :return: list of equivalencies to pass to PhpProject.saveFeatureEquiv
        :rtype: list of str
        """
        selected_presets = []
        for index in self.getSelectedIndexes():
            item_text = self.itemText(index)
            if item_text in list(self.FEATURE_EQUIV_PRESETS):
                selected_presets.append(self.FEATURE_EQUIV_PRESETS[item_text])
        return selected_presets
[docs]    def setADProjectedPointsChecked(self, checked):
        """
        Sets the selected state of the Replace acceptor/donor projected points
        preset item.
        :param checked: whether to select the projected point feature preset
        :type checked: bool
        """
        item = self.FEATURE_DEFINITION_PRESETS[0]
        self.setItemSelected(item, selected=checked)
[docs]    def useADProjectedPointsChecked(self):
        """
        Whether the Replace acceptor/donor projected points item is checked.
        :return: string indicating the number of selected feature presets
        :rtype: str
        """
        all_text = [self.itemText(index) for index in self.getSelectedIndexes()]
        return self.FEATURE_DEFINITION_PRESETS[0] in all_text
[docs]    def currentText(self):
        # See Qt documentation for method documentation
        selected = self.getSelectedItems()
        return '(%i selected)' % len(selected)
[docs]class FeatureMatchCombo(MultiComboBox):
    """
    This class defines special variant of a combo box used in Edit Feature
    dialog to define features that are allowed and forbidden to match. This
    combo box would contain a list of Phase features, which should be
    checkable. Line edit would show comma checked features as a string, which
    contains comma separated one letter feature names. In addition some
    items could be disabled.
    """
[docs]    def __init__(self, parent):
        super(FeatureMatchCombo, self).__init__(parent)
        # List of feature character types, for each combo menu item:
        self.feat_types = []
        item_names = []
        for feature_type in constants.FEATURE_TYPES:
            feature_str = constants.get_feature_text(feature_type)
            item_names.append(feature_str)
            self.feat_types.append(feature_type)
        self.addItems(item_names)
[docs]    def currentText(self):
        """
        Text to show in the combo menu, depending on the current selection.
        Over-rides the standard method of MultiComboBox.
        """
        # See Qt documentation for method documentation
        features = self.checkedFeatures()
        if features:
            features_text = ','.join(features)
        else:
            features_text = 'None'
        return features_text
    def _findFeatureItem(self, feature_type):
        """
        This function finds combo box model item for a given feature type.
        :param feature_type: one letter feature type
        :type feature_type: str
        :return: Row index for a given feature type
        :rtype: int
        """
        return self.feat_types.index(feature_type)
[docs]    def setSelectedFeatures(self, features):
        """
        Select the given features in the combo menu.
        :param features: List of one-letter feature types.
        :type features: list of str
        """
        self.setSelectedIndexes(
            [self._findFeatureItem(feat) for feat in features])
[docs]    def setChecked(self, feature, select):
        """
        This function sets feature item 'checked' state.
        :param feature_type: one letter feature type. 'Enabled' and 'checked'
            states will be set for this feature type.
        :type feature_type: str
        :param checked: boolean indicating whether item should be checked
        :type checked: bool
        """
        index = self._findFeatureItem(feature)
        self.setIndexSelected(index, select)
[docs]    def setEnabled(self, feature_type, enabled):
        """
        This function sets feature item 'enabled' state.
        :param feature_type: one letter feature type. 'Enabled' and 'checked'
            states will be set for this feature type.
        :type feature_type: str
        :param enabled: boolean indicating whether item should be enabled
        :type enabled: bool
        """
        idx = self._findFeatureItem(feature_type)
        self.setIndexEnabled(idx, enabled)
[docs]    def enableAllFeatures(self):
        """
        Set all items to be enabled. Except the features that were "forced" to
        be selected (they are selected and disabled).
        """
        for idx in range(self.count()):
            disabled = not self.isIndexEnabled(idx)
            # We make an exception here for the 'right clicked' feature,
            # which should always remain checked and disabled.
            if disabled and self.isIndexSelected(idx):
                continue
            self.setIndexEnabled(idx, True)
[docs]    def resetAllFeatures(self):
        """
        Resets all item states to enabled and unchecked.
        """
        for idx in range(self.count()):
            self.setIndexEnabled(idx, True)
            self.setIndexSelected(idx, False)
[docs]    def checkedFeatures(self):
        """
        This function returns a list that contains one letter types of
        checked features. Feature that is checked and disabled is the
        'current' feature type. It should be the first item in the list.
        :return: list of checked features
        :rtype: list
        """
        # Find current feature type and make it first element of the list.
        checked = [
            self.feat_types[idx]
            for idx in self.getSelectedIndexes()
            if not self.isIndexEnabled(idx)
        ]
        checked.extend([
            self.feat_types[idx]
            for idx in self.getSelectedIndexes()
            if self.isIndexEnabled(idx)
        ])
        return checked
[docs]class HypothesisRow(object):
    """
    Data class that contains information about entry ids for a given
    Phase hypothesis.
    """
[docs]    def __init__(self, entry_id, hypo):
        """
        Hypothesis data class.
        :param entry_id: hypothesis entry ID
        :type entry_id: int
        :param hypo: hypothesis data object
        :type hypo: `hypothesis.PhaseHypothesis`
        """
        self.entry_id = entry_id
        self.hypo = hypo
        self.num_sites = hypo.getSiteCount()
        # default number of sites to match
        self.min_sites = self.num_sites if self.num_sites < 4 else 4
        self.has_xvol = hypo.hasXvol()
        self.use_xvol = True
class HypothesisColumns(table_helper.TableColumns):
    HypoName = table_helper.Column("Hypothesis")
    MinSites = table_helper.Column(
        "Matches",
        editable=True,
        tooltip="Minimum number of features required for a match")
    ExclVols = table_helper.Column(
        "Excluded Volumes",
        checkable=True,
        tooltip="Toggle on/off usage of excluded volumes in screening.")
[docs]class HypothesisModel(table_helper.RowBasedTableModel):
    """
    Hypotheses Model.
    """
    Column = HypothesisColumns
    ROW_CLASS = HypothesisRow
    @table_helper.data_method(Qt.DisplayRole, Qt.CheckStateRole, Qt.EditRole)
    def _getData(self, col, hypo_row, role):
        # See base class for documentation.
        if col == self.Column.HypoName and role == Qt.DisplayRole:
            return hypo_row.hypo.getHypoID()
        if col == self.Column.ExclVols:
            if role == Qt.CheckStateRole:
                if not hypo_row.has_xvol:
                    return None
                if hypo_row.use_xvol:
                    return Qt.Checked
                else:
                    return Qt.Unchecked
            if role == Qt.DisplayRole:
                if hypo_row.has_xvol:
                    return None
                else:
                    return 'None'
        if col == self.Column.MinSites:
            if role == Qt.CheckStateRole:
                return None
            else:
                return "%d of %d" % (hypo_row.min_sites, hypo_row.num_sites)
    def _setData(self, col, hypo_row, value, role, row_num):
        # See table_helper._setData for method documentation
        if role == Qt.CheckStateRole and col == self.Column.ExclVols:
            hypo_row.use_xvol = bool(value)
            return True
        if col == self.Column.MinSites:
            min_sites, _, _ = value.split()
            hypo_row.min_sites = int(min_sites)
            return True
        return False
    @table_helper.data_method(qt_delegates.ComboBoxDelegate.COMBOBOX_ROLE)
    def _comboBoxContents(self, col, data):
        """
        Data to show in the combo box menu when editing a cell.
        See data_method for argument documentation
        """
        if col == self.Column.MinSites:
            return [
                "%d of %d" % (i, data.num_sites)
                for i in range(2, data.num_sites + 1)
            ]
[docs]    def getAllHypotheses(self):
        """
        Returns a list of all PhaseHypothesis objects in this model.
        :return: All hypotheses
        :rtype: list of `hypothesis.PhaseHypothesis`
        """
        hypos = []
        for row in self.rows:
            hypo = hypothesis.PhaseHypothesis(row.hypo)
            hypo.addProp(phase.PHASE_MIN_SITES, row.min_sites)
            if row.has_xvol and not row.use_xvol:
                hypo.deleteAttr("xvol")
            hypos.append(hypo)
        return hypos
[docs]    def getAllHypoIDs(self):
        """
        Return a list of entry IDs of all hypotheses in the table.
        """
        return set([row.entry_id for row in self.rows])
[docs]class HypothesesListWidget(QtWidgets.QWidget):
    """
    Widget that shows list of Project Table hypotheses. It has a control
    that allows to select hypotheses for 'included' and 'selected' entries.
    :cvar modelChanged: signal emitted when hypotheses are added to this
        widget or deleted.
    :vartype modelChanged: `QtCore.pyqtSignal`
    """
    ADD_HYPO_WS, ADD_HYPO_PT = list(range(2))
    COMBO_TEXT = "Add Hypothesis..."
    modelChanged = QtCore.pyqtSignal()
[docs]    def __init__(self, parent):
        """
        Initialize hypotheses widget.
        """
        super(HypothesesListWidget, self).__init__(parent)
        self._createWidgets()
        self._layoutWidgets()
        self._connectSignals()
    def _createWidgets(self):
        """
        Instantiate all widgets.
        """
        self.hypo_model = HypothesisModel(self)
        self.combo_delegate = qt_delegates.ComboBoxDelegate(self)
        self.hypo_view = table_helper.SampleDataTableView(self)
        header_view = self.hypo_view.horizontalHeader()
        header_view.setStretchLastSection(True)
        self.hypo_view.setSelectionMode(
            QtWidgets.QAbstractItemView.MultiSelection)
        self.hypo_view.setSelectionBehavior(
            QtWidgets.QAbstractItemView.SelectRows)
        self.hypo_view.setModel(self.hypo_model)
        self.hypo_view.setItemDelegateForColumn(self.hypo_model.Column.MinSites,
                                                self.combo_delegate)
        self.source_combo = swidgets.ActionComboBox(self)
        self.source_combo.setText(self.COMBO_TEXT)
        self.source_combo.addItem("Workspace", self.addHypoFromWorkspace)
        if maestro:
            self.source_combo.addItem("Project Table (selected entries)",
                                      self.addHypoFromPT)
        # KNIME-4410: Maestro-less implementation
        self.source_combo.addItem("File...", self._addHypothesisFromFile)
        self.delete_btn = QtWidgets.QPushButton("Delete")
        self.delete_btn.setEnabled(False)
    def _layoutWidgets(self):
        """
        Arrange all widgets
        """
        source_layout = QtWidgets.QHBoxLayout()
        source_layout.addWidget(self.source_combo)
        source_layout.addWidget(self.delete_btn)
        source_layout.addStretch()
        main_layout = QtWidgets.QVBoxLayout()
        main_layout.addLayout(source_layout)
        main_layout.addWidget(self.hypo_view)
        self.setLayout(main_layout)
    def _connectSignals(self):
        """
        Connect widget signals.
        """
        self.delete_btn.clicked.connect(self._deleteHypotheses)
        self.hypo_view.selectionModel().selectionChanged.connect(
            self._selectionChanged)
    def _selectionChanged(self):
        """
        This slot is called when hypothesis selection is changed.
        """
        num_selected = len(self.hypo_view.selectedIndexes())
        self.delete_btn.setEnabled(num_selected > 0)
[docs]    def addHypoFromWorkspace(self):
        """
        Adds hypotheses from Workspace (included Project Table entries).
        """
        for entry_id in self._getHypothesisIDs(self.ADD_HYPO_WS):
            self._addHypothesisFromProject(entry_id)
[docs]    def addHypoFromPT(self):
        """
        Adds hypotheses from the selected Project Table entries.
        """
        for entry_id in self._getHypothesisIDs(self.ADD_HYPO_PT):
            self._addHypothesisFromProject(entry_id)
    def _addHypothesisFromProject(self, entry_id):
        """
        Adds a PT hypothesis to the list, given it's hypothesis ID. If the
        hypothesis is already in the list, this method does nothing.
        :param entry_id: PT entry ID
        :param entry_id: str
        """
        if entry_id in self.hypo_model.getAllHypoIDs():
            return
        hypo = pt_hypothesis.get_hypothesis_from_project(entry_id)
        self.hypo_model.appendRow(entry_id, hypo)
        self.modelChanged.emit()
    def _addHypothesisFromFile(self):
        """
        Adds hypothesis from `*.phypo` or `*_phypo.mae.gz` file.
        """
        filenames = filedialog.get_open_file_names(
            self,
            caption="Select Phase Hypothesis File",
            filter="Phase Hypotheses (*.phypo *_phypo.maegz *_phypo.mae.gz)",
            id="screening_load_hypothesis")
        # Nothing selected
        if not filenames:
            return
        pt = maestro.project_table_get()
        prev_num_entries = len(pt.all_rows)
        for hypo_file in filenames:
            pt.importStructureFile(hypo_file)
            curr_num_entries = len(pt.all_rows)
            for entry_index in range(prev_num_entries, curr_num_entries):
                entry_id = project.ProjectRow(pt, entry_index + 1).entry_id
                if pt_hypothesis.is_hypothesis_entry(entry_id):
                    self._addHypothesisFromProject(entry_id)
            prev_num_entries = curr_num_entries
    def _getHypothesisIDs(self, hypo_from):
        """
        Returns entry ids of hypotheses associated with either selected or
        included rows.
        :param hypo_from: indicates whether hypothesis should be searched
            in selected or included rows.
        :type hypo_from: int
        :return: list of hypothesis ids
        :rtype: list
        """
        if not maestro:
            # unit tests
            return []
        try:
            proj = maestro.project_table_get()
        except ProjectException:
            # Project may have been closed during operation
            return []
        if hypo_from == self.ADD_HYPO_WS:
            hypo_rows = proj.included_rows
        elif hypo_from == self.ADD_HYPO_PT:
            hypo_rows = proj.selected_rows
        # row.entry_id returns a str, but the module uses ints.
        return [
            int(row.entry_id)
            for row in hypo_rows
            if pt_hypothesis.is_hypothesis_entry(row.entry_id)
        ]
    def _deleteHypotheses(self):
        """
        Removes selected items in the hypotheses view.
        """
        self.hypo_model.removeRowsByIndices(self.hypo_view.selectedIndexes())
        self.modelChanged.emit()
[docs]    def addHypothesisFromEntry(self, entry_id):
        """
        Adds a PT hypothesis to the list, given it's entry ID.
        :param entry_id: Project Table entry ID
        :type entry_id: int or str
        """
        self._addHypothesisFromProject(entry_id)
[docs]    def updateHypothesisFromEntry(self, entry_id):
        """
        Updates hypothesis in the model (if it is found) from the PT
        hypothesis with a given entry ID.
        """
        for row in self.hypo_model.rows:
            if row.entry_id == entry_id:
                hypo = pt_hypothesis.get_hypothesis_from_project(entry_id)
                row.hypo = hypo
                self.modelChanged.emit()
                return
[docs]    def getHypotheses(self):
        """
        Returns a list of all hypotheses in the table.
        :return: list of PhaseHypothesis objects.
        :rtype: list
        """
        return self.hypo_model.getAllHypotheses()