Source code for schrodinger.application.bioluminate.antibody.search_results_table

"""
Classes for the search results table used in the Antibody Prediction and
Antibody Humanization: CDR Grafting panels.  These classes cover the table view,
model, and proxy models.
"""

from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt

FRAMEWORK_ROLE = Qt.UserRole
SORT_ROLE = Qt.UserRole + 1


[docs]class FullColumns(object): """ Constants for the full (i.e. non-split) table columns """ NAMES = ('Heavy', 'Light', 'Composite\nScore', 'Antigen Type', 'Species', 'Heavy\nFr. Sim.', 'Light\nFr. Sim.', 'Heavy\nFr. Iden.', 'Light\nFr. Iden.', 'Heavy\nSim.', 'Light\nSim.', 'Heavy\nIden.', 'Light\nIden.', 'PDB\nResolution') TOOLTIPS = ( 'The heavy region template', 'The light region template', 'The average framework region similarity of both heavy and light chain', '\"protein (<N> residues)\"/\"ligand\"/\"none\"', 'The host species of the antibody', 'Sequence similarity of the framework region for the heavy chain', 'Sequence similarity of the framework region for the light chain', 'Sequence identity of the framework region for the heavy chain', 'Sequence identity of the framework region for the light chain', 'Sequence similarity of the entire sequence for the heavy chain', 'Sequence similarity of the entire sequence for the light chain', 'Sequence identity of the entire sequence for the heavy chain', 'Sequence identity of the entire sequence for the light chain', 'PDB Resolution') NUM = len(NAMES) (HEAVY, LIGHT, COMP_SCORE, ANTIGEN_TYPE, SPECIES, HEAVY_FR_SIM, LIGHT_FR_SIM, HEAVY_FR_IDEN, LIGHT_FR_IDEN, HEAVY_SIM, LIGHT_SIM, HEAVY_IDEN, LIGHT_IDEN, PDB_RES) = list(range(NUM)) LIGHT_COLS = (LIGHT, LIGHT_FR_SIM, LIGHT_FR_IDEN, LIGHT_SIM, LIGHT_IDEN)
[docs]class SplitColumns(object): """ Constants for the split table columns (i.e. for a table that shows either the heavy templates or the light templates) """ NUM = 9 (CHAIN, CHAIN_FR_SIM, CHAIN_FR_IDEN, CHAIN_SIM, CHAIN_IDEN, COMP_SCORE, ANTIGEN_TYPE, SPECIES, PDB_RES) = list(range(NUM))
[docs]class TableView(QtWidgets.QTableView): """ A table view for both full and split search results. This table is designed to be used with a FullProxyModel or a SplitProxyModel, as the model is expected to have a DEFAULT_SORT_COLUMN attribute. """
[docs] def __init__(self, parent): super(TableView, self).__init__(parent) self.setSelectionBehavior(QtWidgets.QTableView.SelectRows) self.setSelectionMode(QtWidgets.QTableView.SingleSelection) self.setAlternatingRowColors(True) self.setSortingEnabled(True)
[docs] def reset(self): """ If the model is reset and contains new data, sort the new data appropriately and select the first row. """ if self.model().rowCount(): sort_column = self.model().DEFAULT_SORT_COLUMN sort_order = self.model().DEFAULT_SORT_ORDER self.sortByColumn(sort_column, sort_order) self.horizontalHeader().setSortIndicator(sort_column, sort_order) self.resizeColumnsToContents() # If this method is triggered by a modelReset signal, then the # modelReset signal will trigger a selection model reset as # soon as this method completes, so this selectRow() call may # not have any effect. See PANEL-10098. self.selectRow(0)
[docs] def getSelectedResult(self): """ Get the currently selected framework. This method also causes the selected row to be highlighted in green so the user can see which framework was used to build the current model if they go back to the framework tab. :return: The framework from the currently selected row :rtype: `antibody_prediction_gui.FrameworkTemplate` """ if not self.selectedIndexes(): self.selectRow(0) selected_index = self.selectedIndexes()[0] framework = selected_index.data(FRAMEWORK_ROLE) self.model().setData(selected_index, True) # Store the currently selected row as the accepted row in the proxy # model return framework
[docs]class FullProxyModel(QtCore.QSortFilterProxyModel): """ A proxy model for the full search results. This proxy stores the most recent accepted row, i.e. the framework that was selected when the user clicked Accept. This row will be colored green if the user comes back to the table. :cvar DEFAULT_SORT_COLUMN: When new data is loaded into a table using this proxy, it will be initially sorted using this column. :vartype DEFAULT_SORT_COLUMN: int :cvar SECONDARY_SORT_COLUMNS: When two rows contain identical values for a given sort column, those rows will be sorted using SECONDARY_SORT_COLUMNS[column] :cvar ACCEPTED_FRAMEWORK_COLOR: The background color for the accepted framework row. :vartype ACCEPTED_FRAMEWORK_COLOR: `PyQt5.QtGui.QColor` """ COLUMN = FullColumns() DEFAULT_SORT_COLUMN = COLUMN.COMP_SCORE DEFAULT_SORT_ORDER = Qt.DescendingOrder SECONDARY_SORT_COLUMNS = { COLUMN.HEAVY_SIM: COLUMN.COMP_SCORE, COLUMN.LIGHT_SIM: COLUMN.COMP_SCORE, COLUMN.HEAVY_IDEN: COLUMN.HEAVY_SIM, COLUMN.LIGHT_IDEN: COLUMN.LIGHT_SIM, COLUMN.HEAVY_FR_SIM: COLUMN.COMP_SCORE, COLUMN.LIGHT_FR_SIM: COLUMN.COMP_SCORE, COLUMN.HEAVY_FR_IDEN: COLUMN.HEAVY_FR_SIM, COLUMN.LIGHT_FR_IDEN: COLUMN.LIGHT_FR_SIM } ACCEPTED_FRAMEWORK_COLOR = QtGui.QColor() ACCEPTED_FRAMEWORK_COLOR.setHsvF(0.25, 1.0, 0.5, 0.5)
[docs] def __init__(self, parent): super(FullProxyModel, self).__init__(parent) self.setSortRole(SORT_ROLE) self.accepted_row = None # maintain Qt4 dynamicSortFilter default self.setDynamicSortFilter(False)
[docs] def setData(self, index, value, role=Qt.EditRole): """ Store the accepted framework :param index: The index of a cell from the row to set as the accepted framework. The column of the index is ignored. :type index: `PyQt5.QtCore.QModelIndex` :param value: The value to set. Is expected to be True to set the accepted framework. :type value: bool :param role: The role to set data for. Is expected to be Qt.EditRole to set the accepted framework. :type role: int :return: True if the accepted framework was stored successfully. False otherwise. :rtype: bool """ if value and role == Qt.EditRole: self.accepted_row = self.mapToSource(index).row() left_index = self.index(index.row(), 0) right_index = self.index(index.row(), self.columnCount()) self.dataChanged.emit(left_index, right_index) return True else: return super(FullProxyModel, self).setData(index, value, role)
[docs] def data(self, index, role=Qt.DisplayRole): """ Color the background of the accepted framework row green, and pass through all other data from the model. :param index: The index to retrieve data for :type index: `PyQt5.QtCore.QModelIndex` :param role: The role to retrieve data for :type role: int :return: The requested data """ if role == Qt.BackgroundRole: if self.mapToSource(index).row() == self.accepted_row: return self.ACCEPTED_FRAMEWORK_COLOR else: return super(FullProxyModel, self).data(index, role)
[docs] def setSourceModel(self, source_model): """ Set the source model for this proxy, and make sure that the accepted row is reset when the model is reset. :param source_model: The source model to set :type source_model: `FrameworkModel` """ super(FullProxyModel, self).setSourceModel(source_model) source_model.modelReset.connect(self.resetAcceptedRow)
[docs] def resetAcceptedRow(self): """ Reset the accepted row. """ self.accepted_row = None
[docs] def lessThan(self, source_left, source_right): """ See Qt documentation for method documentation. """ sort_role = self.sortRole() source_model = self.sourceModel() if source_left.data(sort_role) == source_right.data(sort_role): column = source_left.column() if column in self.SECONDARY_SORT_COLUMNS: new_column = self.SECONDARY_SORT_COLUMNS[column] new_source_left = source_model.index(source_left.row(), new_column) new_source_right = source_model.index(source_right.row(), new_column) return self.lessThan(new_source_left, new_source_right) return super(FullProxyModel, self).lessThan(source_left, source_right)
[docs]class SplitProxyModel(FullProxyModel): """ A proxy for the split search results (i.e. for a table that shows either the heavy templates or the light templates). :ivar col_from_source: A mapping of {column number in the source model: column number in this proxy model} :vartype col_from_source: dict :ivar col_to_source: A mapping of {column number in this proxy model: column number in the source model} :vartype col_to_source: dict """ COLUMN = SplitColumns() DEFAULT_SORT_COLUMN = COLUMN.CHAIN_FR_SIM
[docs] def __init__(self, parent, is_heavy): """ Initialize the proxy :param parent: The Qt parent widget :type parent: `PyQt5.QtWidgets.QWidget` :param is_heavy: Does this proxy model represent the heavy chain (True) or the light chain (False)? :type is_heavy: bool """ super(SplitProxyModel, self).__init__(parent) self.is_heavy = is_heavy if is_heavy: chain_col = FullColumns.HEAVY chain_fr_sim_col = FullColumns.HEAVY_FR_SIM chain_fr_iden_col = FullColumns.HEAVY_FR_IDEN chain_sim_col = FullColumns.HEAVY_SIM chain_iden_col = FullColumns.HEAVY_IDEN else: chain_col = FullColumns.LIGHT chain_fr_sim_col = FullColumns.LIGHT_FR_SIM chain_fr_iden_col = FullColumns.LIGHT_FR_IDEN chain_sim_col = FullColumns.LIGHT_SIM chain_iden_col = FullColumns.LIGHT_IDEN reso_col = FullColumns.PDB_RES self._accepted_cols = set( (chain_col, chain_fr_sim_col, chain_fr_iden_col, chain_sim_col, chain_iden_col, reso_col))
[docs] def filterAcceptsColumn(self, source_column, source_parent=None): return source_column in self._accepted_cols
[docs]class FrameworkModel(QtCore.QAbstractTableModel): """ A model to store search results """ COLUMN = FullColumns()
[docs] def __init__(self, parent): super(FrameworkModel, self).__init__(parent) self.results = []
[docs] def headerData(self, section, orientation, role): """ Retrieve the requested header data :param section: The row/column number to retrieve header data for :type section: int :param orientation: The orientation of the header (Qt.Horizontal or Qt.Vertical) to retrieve data for :type orientation: int :param role: The role to retrieve header data for :type role: int """ if orientation != Qt.Horizontal: # There's no vertical header return elif role == Qt.DisplayRole: return self.COLUMN.NAMES[section] elif role == Qt.FontRole: font = QtGui.QFont() font.setBold(True) return font elif role == Qt.ToolTipRole: return self.COLUMN.TOOLTIPS[section]
[docs] def rowCount(self, parent=None): """ Return the number of rows in the model :param parent: Unused, but present for PyQt compatibility. :return: The number of rows in the model :rtype: int """ return len(self.results)
[docs] def columnCount(self, parent=None): """ Return the number of columns in the model :param parent: Unused, but present for PyQt compatibility. :return: The number of columns in the model :rtype: int """ return self.COLUMN.NUM
[docs] def loadData(self, results): """ Load in new data, replacing any existing data :param results: The results of the framework search. Must be a list of `antibody_prediction_gui.FrameworkTemplate` objects. :type results: list """ self.beginResetModel() self.results = results self.endResetModel()
[docs] def reset(self): """ Remove any existing data """ self.beginResetModel() self.results = [] self.endResetModel()
[docs] def data(self, index, role=Qt.DisplayRole): """ Retrieve the requested data :param index: The index to retrieve data for :type index: `PyQt5.QtCore.QModelIndex` :param role: The role to retrieve data for :type role: int :return: The requested data """ if role == Qt.TextAlignmentRole: return Qt.AlignCenter elif role == FRAMEWORK_ROLE: row = index.row() return self.results[row] elif role in (Qt.DisplayRole, SORT_ROLE): row = index.row() col = index.column() result = self.results[row] if col in self.COLUMN.LIGHT_COLS: if result.isSingleDomain(): return '' if col == self.COLUMN.HEAVY: return result.alignments['Heavy'].title elif col == self.COLUMN.LIGHT: return result.alignments['Light'].title elif col == self.COLUMN.COMP_SCORE: return self._formatData(result.score, role) elif col == self.COLUMN.ANTIGEN_TYPE: ag_type = str(result.antigenType) if ag_type == "protein": num_res = len(result.antigenSeq) if role == Qt.DisplayRole: res_format = "" elif role == SORT_ROLE: # When sorting, pad the number of residues with zeros so # that protein antigens get properly sorted (i.e. so a # protein antigen with 99 residues is sorted before a # protein antigen with 100 residues) res_format = "06" ag_type += " ({0:{1}} residues)".format(num_res, res_format) return ag_type elif col == self.COLUMN.SPECIES: return result.getSpecies() elif col == self.COLUMN.HEAVY_FR_SIM: return self._formatData( result.alignments['Heavy'].similarity_fr, role) elif col == self.COLUMN.LIGHT_FR_SIM: return self._formatData( result.alignments['Light'].similarity_fr, role) elif col == self.COLUMN.HEAVY_FR_IDEN: return self._formatData(result.alignments['Heavy'].identity_fr, role) elif col == self.COLUMN.LIGHT_FR_IDEN: return self._formatData(result.alignments['Light'].identity_fr, role) elif col == self.COLUMN.HEAVY_SIM: return self._formatData(result.alignments['Heavy'].similarity, role) elif col == self.COLUMN.LIGHT_SIM: return self._formatData(result.alignments['Light'].similarity, role) elif col == self.COLUMN.HEAVY_IDEN: return self._formatData(result.alignments['Heavy'].identity, role) elif col == self.COLUMN.LIGHT_IDEN: return self._formatData(result.alignments['Light'].identity, role) elif col == self.COLUMN.PDB_RES: if result.resolution is None: return 'N/A' elif isinstance(result.resolution, float): return self._formatData(result.resolution, role) else: # If the resolution came from s_bioluminate_PDB_EXPDTA return result.resolution
def _formatData(self, data, role): """ Format numerical data for either the display role (two digits after the decimal) or the sort role (full precision). :param data: The data to format :type data: float :param role: The role to format the data for. Should be either Qt.DisplayRole or SORT_ROLE :type role: int :return: The formatted data :rtype: str or float """ if role == Qt.DisplayRole: return "%.2f" % data elif role == SORT_ROLE: return data
[docs] def flags(self, index): """ Retrieve flags for the specified index :param index: The index to retrieve data for :type index: `PyQt5.QtCore.QModelIndex` :return: The flags for the specified index :rtype: int """ return Qt.ItemIsSelectable | Qt.ItemIsEnabled
[docs] def isPopulated(self): """ Does this model contain data (i.e. > 0 rows)? :return: True if the model contain data, False otherwise :rtype: bool """ return bool(self.results)
[docs]class FullTable(QtWidgets.QWidget): """ A widget to contain the full search results table """ COLUMN = FullColumns() PROXY_CLASS = FullProxyModel VIEW_CLASS = TableView
[docs] def __init__(self, parent, model): """ Initialize the table using the specified model :param parent: The Qt parent widget :type parent: `PyQt5.QtWidgets.QWidget` :param model: The model containing the search results :type model: `FrameworkModel` """ QtWidgets.QWidget.__init__(self, parent) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) self.proxy = self.PROXY_CLASS(parent) self.proxy.setSourceModel(model) self.view = self.VIEW_CLASS(parent) self.view.setModel(self.proxy) layout.addWidget(self.view)
[docs] def getSelectedResult(self): """ Get the currently selected framework :return: The framework from the currently selected row :rtype: `antibody_prediction_gui.FrameworkTemplate` """ return self.view.getSelectedResult()
[docs] def updateLightColVisibility(self, show_light): """ Show or hide light columns when single-domain prediction is toggled. :param show_light: Whether or not to show the light columns. :param show_light: bool """ update = self.view.showColumn if show_light else self.view.hideColumn for col in self.COLUMN.LIGHT_COLS: update(col)
[docs]class SplitTable(QtWidgets.QWidget): """ A widget to contain both split search results tables (i.e. one table that show the heavy frameworks and one table that shows the light frameworks). """ COLUMN = SplitColumns()
[docs] def __init__(self, parent, model): """ Initialize the tables using the specified model :param parent: The Qt parent widget :type parent: `PyQt5.QtWidgets.QWidget` :param model: The model containing the search results :type model: `FrameworkModel` """ QtWidgets.QWidget.__init__(self, parent) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) self.heavy_proxy = SplitProxyModel(parent, True) self.heavy_proxy.setSourceModel(model) self.heavy_view = TableView(parent) self.heavy_view.setModel(self.heavy_proxy) layout.addWidget(self.heavy_view) self.light_proxy = SplitProxyModel(parent, False) self.light_proxy.setSourceModel(model) self.light_view = TableView(parent) self.light_view.setModel(self.light_proxy) layout.addWidget(self.light_view)
[docs] def getSelectedResults(self): """ Get the currently selected frameworks from both tables :return: A list of the currently selected [heavy framework, light framework] :rtype: list """ frameworks = [] for view in self.heavy_view, self.light_view: cur_framework = view.getSelectedResult() frameworks.append(cur_framework) return frameworks