Source code for schrodinger.application.bioluminate.protein_report_widget

"""
Analyzes the quality of the structure in the workspace
in terms of bond length, bond angles, dihedral values,
peptide geometry, and other quantities.

Copyright (c) Schrodinger, LLC. All rights reserved.
"""
# Contributors: Tyler Day, Matvey Adzhigirey, Dave Giesen

import schrodinger.protein.analysis as prosane
import schrodinger.ui.qt.filedialog as filedialog
import schrodinger.ui.qt.swidgets as swidgets
from schrodinger import get_maestro
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import appframework
from schrodinger.ui.qt import messagebox
from schrodinger.ui.qt.utils import wait_cursor

maestro = get_maestro()


[docs]class DataModel(QtCore.QAbstractTableModel): """ Class for storing the table information. """
[docs] def __init__(self, master): """ Create a DataModel instance """ QtCore.QAbstractTableModel.__init__(self) self.master = master self._data = [] self._headers = [] self.summary = None self.view_atoms = None
[docs] def rowCount(self, parent=QtCore.QModelIndex()): # noqa: M511 """ Returns number of rows :type parent: QtCore.QModelIndex :param parent: unused :rtype: int :return: Number of rows """ return len(self._data)
[docs] def columnCount(self, parent=QtCore.QModelIndex()): # noqa: M511 """ Returns number of columns :type parent: QtCore.QModelIndex :param parent: unused :rtype: int :return: Number of rows """ return len(self._headers)
[docs] def setData(self, data_list, headers): """ Sets the internal data list to the specified list. NOTE: Will use the actual list passed to it (without making a copy) :type data_list: list :param data_list: the list of data for the model. Uses the actual list, not a copy :type headers: list of str :param headers: The column headers """ self.beginResetModel() self._headers = headers self._data = data_list self.endResetModel()
[docs] def data(self, index, role=Qt.DisplayRole): """ Given a cell index, returns the data that should be displayed in that cell. Used by the view. :type index: QtCore.QModelIndex :param index: index of the cell to return data for :type role: Qt.ItemDataRole :param role: The data role to return :return: the requested data for the requested index """ if role == Qt.DisplayRole: row_data = self._data[index.row()] col = index.column() return row_data[col]
[docs] def headerData(self, section, orientation, role): """ Returns the string that should be displayed in the specified header cell. Used by the View. :type section: int :param section: the row or column index requested :type orientation: Qt.Orientation (Qt.Horizontal or Qt.Vertical) :param orientation: Whether this is for the horizontal or vertical header :type role: Qt.ItemDataRole :param role: The data role to return - Qt.DisplayRole to return the text of the header. :return: the requested data for the requested header item """ if orientation == Qt.Horizontal: if role == Qt.DisplayRole: try: return self._headers[section] except IndexError: return
[docs] def sort(self, Ncol, order): """ Sort table by given column number. The first column will be sorted based on the first chain:residue number rather than a simple alphabetical sort. Note that this method changes the actual model._data property. :type Ncol: int :param Ncol: The column index to sort :type order: Qt.SortOrder (Qt.AscendingOrder or Qt.DescendingOrder) :param order: The order in which to sort the column """ def residue_formatter(item): """ Column 0 labels have various formats, but all of them start with Chain:ResName ResNum. A simple sort sorts on ResName before ResNum, but that isn't very useful. This sorts on Chain:ResNum :rtype: str :return: key to sort item by """ try: label = item[0] except IndexError: return "" try: chain, remainder = label.split(':', 1) tokens = remainder.split() number = tokens[1].strip(':') number = number.zfill(4) except (ValueError, IndexError): return label return ':'.join([chain, number]) self.layoutAboutToBeChanged.emit() if Ncol == 0: key = residue_formatter else: # assign key for sorting the row column value if value is a float or int # assign -inf for sorting comparisons if other datatype key = lambda row: row[Ncol] if isinstance(row[Ncol], (float, int) ) else float("-inf") self._data = sorted(self._data, key=key) if order == Qt.DescendingOrder: self._data.reverse() self.layoutChanged.emit()
[docs]class ReportFrame(QtWidgets.QFrame): """ A QtWidgets.QFrame that contains a Protein Report """
[docs] def __init__(self, parent_layout=None, update_button=True, app_parent=None, load_proteins=True): """ Create a ReportFrame object :type parent_layout: QLayout or None :param parent_layout: If given the ReportFrame will add itself to this layout :type update_button: bool :param update_button: True if the report should have an update button, False if not. :type app_parent: Deprecated; no longer used. :param app_parent: Deprecated; no longer used. :type load_proteins: bool :param load_proteins: If True, the panel will attempt to load proteins when created. If False (default), not attempt is made - the widgets are merely created. """ QtWidgets.QFrame.__init__(self) if parent_layout is not None: parent_layout.addWidget(self) self.layout = swidgets.SVBoxLayout(self) self.entry_name_label = swidgets.SLabel("", layout=self.layout) self.models = {} # Key: title, value: Model self.currently_displayed_title = None self.syncd = True self.previous_asl = None # Must create now so it exists during initialization self.summary_ledit = swidgets.SLabeledEdit('Structure average:') self.summary_ledit.setReadOnly(True) # Top button row top_layout = swidgets.SHBoxLayout() self.layout.addLayout(top_layout) self.set_combo = swidgets.SLabeledComboBox('Display:', command=self.setChanged, layout=top_layout) self.set_combo.setSizeAdjustPolicy(self.set_combo.AdjustToContents) self.export_button = swidgets.SPushButton( 'Export...', command=self.exportCurrentTable, layout=top_layout) self.export_button = swidgets.SPushButton('Export All...', command=self.exportAllTables, layout=top_layout) # Setup the table: self.table = QtWidgets.QTableView() self.table.verticalHeader().hide() self.table.setSelectionBehavior(self.table.SelectRows) self.table.setSelectionMode(self.table.ExtendedSelection) self.table.setSortingEnabled(True) self.layout.addWidget(self.table) # Summary area self.bottom_layout = swidgets.SHBoxLayout() self.layout.addLayout(self.bottom_layout) self.bottom_layout.addLayout(self.summary_ledit.mylayout) # Update button if requested if update_button: update_layout = swidgets.SHBoxLayout() self.update_button = swidgets.SPushButton('Update', command=self.updateTables, layout=update_layout) update_layout.addStretch() self.layout.addLayout(update_layout) else: self.update_button = None if maestro: if load_proteins: self.updateTables() maestro.workspace_changed_function_add(self.workspaceChanged)
# Removed highlighting due to EV 117692 #self.highlight = 'Protein_Report_Highlight' #maestro.command('highlight ' + self.highlight) #maestro.command('highlightmethod ' + self.highlight + #' type=silhouette')
[docs] def getSelectedDataTitles(self, model): """ Returns a list of selected data titles (the value in the first column) :type model: DataModel :param model: The model the data comes from :rtype: list :return: list of data titles (the value in the first column) for each selected row """ selection_model = self.table.selectionModel() selected_rows = [x.row() for x in selection_model.selectedRows()] # Get the selected row data, first column: selected_data = [model._data[x][0] for x in selected_rows] return selected_data
[docs] def selectionChangedCallback(self, selected=None, deselected=None): """ Called when table selection is changed :type selected: QItemSelection :param selected: Unused callback-required parameters :type deselected: QItemSelection :param deselected: Unused callback-required parameters """ if not self.syncd: msg = "Workspace has changed, and data may no longer be valid." if self.update_button: msg = msg + " Please use Update button." maestro.warning(msg) return model = self.models[self.currently_displayed_title] selected_data = self.getSelectedDataTitles(model) selected_atom_list = [] for data in selected_data: selected_atom_list.extend(model.view_atoms[data]) selected_atoms = set(selected_atom_list) # Change the previously 'tubed' atoms back to wire' if self.previous_asl: maestro.command('repatom rep=none ' + self.previous_asl) maestro.command('repbond rep=wire') maestro.command('repatombonds ' + self.previous_asl) if not selected_atoms: maestro.command("workspaceselectionclear") self.previous_asl = None # Removed highlighting due to EV 117692 #maestro.command("highlighthide " + self.highlight) return atom_list = ",".join([str(x) for x in selected_atoms]) asl = ' atom.num %s' % atom_list # Place the atoms front and center maestro.command("spotcenter %s" % atom_list) maestro.command('fit fillres(' + asl + ')') # Select them and highlight them maestro.command("workspaceselectionreplace" + asl) # Removed highlighting due to EV 117692 # maestro.command('highlightatoms ' + self.highlight + asl) # Show the atoms as tubes maestro.command('ribbon style=none ' + asl) maestro.command('displayatom ' + asl) maestro.command('repatom rep=none ' + asl) maestro.command('repbond rep=tube') maestro.command('repatombonds ' + asl) self.previous_asl = asl
[docs] def getDataModelClass(self): """ Returns the proper data model class to use, allows easy subclassing of the Model in report subclasses :rtype: DataModel :return: The CLASS used for the table data model in the report. Does not return a class instance, only the class itself. """ return DataModel
[docs] def getStructure(self, entry_id=None): """ Return the structure to report on. :type entry_id: str or None :param entry_id: Entry ID of the structure to use, or None (default) if the workspace structure is to be used. If entry_id is given, the structure is taken from the project table row for that entry_id (which may be different from the current workspace structure). If entry_id is None, the workspace structure is used, but crystal structure properties are taken from the included project table row :rtype: `schrodinger.structure.Structure` :return: The structure requested """ struct = None pt = maestro.project_table_get() if entry_id is None: # Use the workspace structure, but get the CT from the Project Table # so that we can get crystal properties pt_struct = None title = 'Scratch Entry' for row in pt.all_rows: if row.in_workspace: if pt_struct is not None: # Already have one. messagebox.show_warning( parent=self, text= "ERROR: more than 1 entry is included in the workspace." ) return struct pt_struct = row.getStructure() title = row.title # Get the structure itself from the workspace, as it may be modified # but not synced. struct = maestro.workspace_get() self.entry_name_label.setText(title) # Copy over crystal properties, if they exist. if pt_struct is not None: self.entry_name_label.setText(title) for propname in [ 's_pdb_PDB_CRYST1_Space_Group', 'r_pdb_PDB_CRYST1_a', 'r_pdb_PDB_CRYST1_b', 'r_pdb_PDB_CRYST1_c', 'r_pdb_PDB_CRYST1_alpha', 'r_pdb_PDB_CRYST1_beta', 'r_pdb_PDB_CRYST1_gamma' ]: try: struct.property[propname] = pt_struct.property[propname] except KeyError: pass else: struct = pt.getRow(entry_id).getStructure() return struct
[docs] @wait_cursor def updateTables(self, entry_id=None): """ Calculate new data for the requested structure and load the data into the table. :type entry_id: str or None :param entry_id: Entry ID of the structure to use, or None (default) if the workspace structure is to be used. If entry_id is given, the structure is taken from the project table row for that entry_id (which may be different from the current workspace structure). If entry_id is None, the workspace structure is used, but crystal structure properties are taken from the included project table row """ # Get the structure and the report data struct = self.getStructure(entry_id=entry_id) if struct is None: return self.analysis = prosane.Report(struct) # Setup the models based on the data from prosane.Report: self.models = {} all_titles = [] all_tooltips = [] for data_set in self.analysis.data: all_titles.append(data_set.title) try: all_tooltips.append(data_set.tooltip) except AttributeError: all_tooltips.append(data_set.title) model = self.getDataModelClass()(self) model.view_atoms = {} for point in data_set.points: model.view_atoms[point.descriptor] = point.atoms table_data = [] for point in data_set.report_data_points(): formatted_data = [] for field in point: if isinstance(field, float): formatted_data.append(round(field, 3)) else: formatted_data.append(field) table_data.append(formatted_data) headers = self.analysis.table_headers[data_set.title] model.setData(table_data, headers) if isinstance(data_set.summary, float): model.summary = str(round(data_set.summary, 3)) else: model.summary = str(data_set.summary) self.models[data_set.title] = model self.setSets(all_titles, all_tooltips) self.syncd = True
[docs] def clear(self): """ Clear the widget. Used by PrepWizard. """ self.summary_ledit.setText('N/A') self.table.setModel(None) self.models = {} self.setSets([], []) self.currently_displayed_title = None
[docs] def setSets(self, all_titles, all_tooltips): """ Populate the sets menu with the given items. Also takes in a list of tooltips, for each item. :param all_titles: List of item texts to add. :type all_titles: list(str) :param all_tooltips: List of tool tips for each item. :type: all_tooltips: list(str) """ self.set_combo.clear() i = 0 for title, tooltip in zip(all_titles, all_tooltips): self.set_combo.addItem(title) self.set_combo.setItemData(i, tooltip, Qt.ToolTipRole) i = i + 1
[docs] @wait_cursor def exportTables(self, filename, tables_to_write): """ Export the specified tables to the <filename> file. :type filename: str :param filename: the path to the output file :type tables_to_write: str :param tables_to_write: the name of the set of data to write out """ filehandle = open(filename, 'w') for table_name in tables_to_write: model = self.models[table_name] filehandle.write("%-40s\n" % table_name) filehandle.write("%-30s" % self.analysis.table_headers[table_name][0]) for header in self.analysis.table_headers[table_name][1:]: filehandle.write("%-10s" % header) filehandle.write("\n") for line in model._data: filehandle.write("%-30s" % line[0]) for value in line[1:]: filehandle.write("%-10s" % str(value)) filehandle.write("\n") filehandle.write("\n") filehandle.close()
[docs] def setChanged(self, selected_title): """ Callback for when a new set of data is requested. Changes the data displayed in the table. This also changes the underlying model the table uses. :type selected_title: str :param selected_title: The name of the set of data requested """ # Convert to a Python string: selected_title = str(selected_title) self.currently_displayed_title = selected_title try: model = self.models[self.currently_displayed_title] except KeyError: # No data available return self.table.setModel(model) self.table.resizeColumnsToContents() self.summary_ledit.setText(str(model.summary)) selection_model = self.table.selectionModel() selection_model.selectionChanged.connect(self.selectionChangedCallback) # This moves the sort indicator in the header self.table.sortByColumn(0, Qt.AscendingOrder)
[docs] def exportCurrentTable(self): """ Export only the current table. """ filename = filedialog.get_save_file_name( self, "Save the current table as", ".", # initial dir "Text file (*.txt)", ) if not filename: return self.exportTables(filename, [self.currently_displayed_title])
[docs] def exportAllTables(self): """ Export all tables. """ filename = filedialog.get_save_file_name( self, "Save all tables as", ".", # initial dir "Text file (*.txt)", ) if not filename: return self.exportTables(filename, self.analysis.table_names)
[docs] def openHelp(self): """ Display the help topic """ maestro.command("helptopic TOOLS_MENU_PROTEIN_REPORTS_PANEL") return
[docs] def closePanel(self): """ Hide the panel """ if maestro: maestro.workspace_changed_function_remove(self.workspaceChanged) # Will quit if outside of Maestro, hide if in Maestro: appframework.AppFramework.closePanel(self)
[docs] def workspaceChanged(self, changed): """ Keep track of when something changes in the workspace that would invalidate the current data. :type changed: str :param changed: What changed in the workspace """ if changed in [ maestro.WORKSPACE_CHANGED_EVERYTHING, maestro.WORKSPACE_CHANGED_GEOMETRY, maestro.WORKSPACE_CHANGED_COORDINATES, maestro.WORKSPACE_CHANGED_CONNECTIVITY, maestro.WORKSPACE_CHANGED_APPEND ]: self.syncd = False else: self.syncd = True return
[docs] def close(self): """ Close the frame, and clean up anything that needs to happen upon closing """ #if maestro: #maestro.command('highlightdelete ' + self.highlight) return QtWidgets.QFrame.close(self)
[docs]def panel(): """ Open (or re-open) the panel """ global app if app: app.show() else: app = ReportFrame() # create new App instance app.show()
[docs]def quit(): """ To force quit the GUI in Maestro, call 'app.quit' """ global app if app: app.quitPanel() # will quit even if in maestro app = None
# For debugging purposes: if __name__ == '__main__': app = ReportFrame() app.show() app.exec() #EOF