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