Source code for schrodinger.ui.qt.pdb_dialog

"""
Contains class that is used to display "Get Pdb Dialog".
"""

import os
import requests
from contextlib import contextmanager
from schrodinger import structure
from schrodinger.utils import fileutils
from schrodinger.protein import getpdb
from schrodinger.protein import biounit
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import widgetmixins
from schrodinger.utils.documentation import show_topic

from . import pdb_dialog_ui
from . import stylesheet

PATH_SEPARATOR = ';'


[docs]def validate_pdb_id(pdb_id): """ Return True if given string is a valid PDB ID: 4 characters beginning with a digit, and characters 2-4 should be alphanumeric. """ # TODO consider moving to getpdb.py module return len(pdb_id) == 4 and pdb_id.isalnum() and pdb_id[0].isdigit()
[docs]def extract_chain(filename, chain_name): """ Extract the specific chain from the given protein structure file. Raises KeyError if chain is not present. :param filename: Name of the protein file. :type filename: str :param chain_name: Chain to be extracted. :type chain_name: str :return : Structure of the extracted chain :rtype: schrodinger.structure.Structure """ st = structure.Structure.read(filename) chain_st = st.chain[chain_name].extractStructure(copy_props=True) return chain_st
[docs]def create_biounits(filename): """ Generate biological assemblies, and write a single Maestro file with multiple structures. Maestro is used instead of PDB mainly because PDB format doesn't support custom title properties. Assembly number will be added to the title of each output structure. """ st = structure.Structure.read(filename) biounits = biounit.biounits_from_structure(st) if not biounits: return filename basename, ext = fileutils.splitext(filename) outfile = basename + '_bio.maegz' with structure.StructureWriter(outfile) as writer: for i, bu in enumerate(biounits, start=1): bu_st = biounit.apply_biounit(st, bu) if len(biounits) > 1: bu_st.title = f'{bu_st.title}-{i}' writer.append(bu_st) return outfile
[docs]class PDBDialog(widgetmixins.MessageBoxMixin, QtWidgets.QDialog): """ A QDialog to download Pdb file from pdb_id given by user. """
[docs] def __init__(self): QtWidgets.QDialog.__init__(self, None) self.ui = pdb_dialog_ui.Ui_PdbDialog() self.ui.setupUi(self) self.setupFetchingOptions() self.ui.change_btn.setStyleSheet(stylesheet.OPTIONS_BUTTON) self.updateDownloadButton() self.pdb_filepath = "" self.saved_chain_names = "" self.ui.download_button.clicked.connect(self.downloadFile) self.ui.help_button.clicked.connect(self.getHelp) self.ui.cancel_button.clicked.connect(self.cancel) self.ui.pdb_id_text.textChanged.connect(self.updateDownloadButton) self.ui.biological_unit.toggled.connect(self.onBiologicalUnitToggled) self.geometry = QtCore.QByteArray()
[docs] def setupFetchingOptions(self): """ Set up fetching options menu. """ self.ui.change_btn.setPopupMode(QtWidgets.QToolButton.InstantPopup) fetch_group = QtWidgets.QActionGroup(self) self.auto_fetch = QtWidgets.QAction("Local or Web") self.retrieve_from_local = QtWidgets.QAction("Local Installation Only") self.download_from_web = QtWidgets.QAction("Web Only") fetch_menu = QtWidgets.QMenu() fetch_menu_items = (self.auto_fetch, self.retrieve_from_local, self.download_from_web) for item in fetch_menu_items: fetch_menu.addAction(item) fetch_group.addAction(item) item.setCheckable(True) self.ui.change_btn.setMenu(fetch_menu) fetch_menu.adjustSize() self.auto_fetch.setChecked(True) self.auto_fetch.toggled.connect( lambda: self.ui.fetch_label.setText("Fetching from: Local or Web")) self.download_from_web.toggled.connect( lambda: self.ui.fetch_label.setText("Fetching from: Web")) self.retrieve_from_local.toggled.connect( self.onRetrieveFromLocalToggled)
[docs] def onBiologicalUnitToggled(self, checked): """ Respond to toggling of 'Biological unit' checkbox. :param checked: whether 'Biological unit' checkbox is checked or not :type checked: bool """ if checked: self.saved_chain_names = self.ui.chain_name_text.text() self.ui.chain_name_text.clear() self.ui.chain_name_text.setEnabled(False) self.ui.d_chain_name.setEnabled(False) else: self.ui.chain_name_text.setEnabled(True) self.ui.chain_name_text.setText(self.saved_chain_names) self.ui.d_chain_name.setEnabled(True)
[docs] def onRetrieveFromLocalToggled(self, checked): """ Respond to toggling of 'Local Installation Only' menu item. :param checked: whether 'Local Installation Only' menu item is checked or not :type checked: bool """ if checked: self.ui.biological_unit.setChecked(False) self.ui.biological_unit.setEnabled(False) self.ui.fetch_label.setText("Fetching from: Local Installation") else: self.ui.biological_unit.setEnabled(True)
[docs] def getHelp(self): """ Show help documentation of functionality of dialog. """ show_topic("PROJECT_MENU_GET_PDB_FILE_DB")
[docs] def updateDownloadButton(self): """ Disable the Download button if there is no PDB ID, else enable it. """ enable = bool(str(self.ui.pdb_id_text.text())) self.ui.download_button.setEnabled(enable)
[docs] @contextmanager def manageDownloadFile(self): try: self.setCursor(Qt.WaitCursor) self.ui.pdb_id_text.setEnabled(False) self.ui.download_button.setEnabled(False) yield finally: self.setCursor(Qt.ArrowCursor) self.ui.download_button.setEnabled(True) self.ui.pdb_id_text.setEnabled(True)
def _getPDBIDs(self): """ Get the pdb id(s) from the GUI. :return: list of pdb ids :rtype : list of str """ file_text = self.ui.pdb_id_text.text().strip() if not file_text: return [] # Support both spaces and commas for splitting PDB IDs: file_text = file_text.lower().replace(",", " ") pdb_id_list = file_text.split() return pdb_id_list def _getPDB(self): """ Retrieve PDB file(s) specified in the UI. Returns list of files written on success, or None on failure. """ biological_unit = self.ui.biological_unit.isChecked() diff_data = self.ui.diffraction_data.isChecked() local_only = self.retrieve_from_local.isChecked() remote_only = self.download_from_web.isChecked() if remote_only: source = getpdb.WEB elif local_only: source = getpdb.DATABASE else: source = getpdb.AUTO pdb_id_list = self._getPDBIDs() if not pdb_id_list: self.warning("PDB ID is not specified", "Missing PDB ID") return None # Add list of PDB ids + chain ids to download chain_name = self.ui.chain_name_text.text() if len(chain_name) > 1: self.warning('Chain name must be a single character.') return None invalid_pdbs = [id for id in pdb_id_list if not validate_pdb_id(id)] if invalid_pdbs: self.warning('Invalid PDB ID: %s' % ', '.join(invalid_pdbs)) return None downloaded_files = [] for pdb_id in pdb_id_list: fname = self._downloadPdb(source, pdb_id, chain_name, biological_unit, diff_data) if not fname: continue downloaded_files.append(fname) return downloaded_files def _downloadPdb(self, source, pdb_id, chain_name, biological_unit, diff_data): """ Download a given PDB, and return path to written file. :param pdb_id: PDB ID :type pdb_id: str :param chain_name: Chain name to retain, or None to keep all. :type chain: str :param biological_unit: Whether to return biological complex (1st one of all available). :type biolotical_unit: bool :param diff_data: Whether to also download reflecton data file (*.cv, with same basename as the PDB file). :type diff_data: bool :return: Returns the file path to the written file, or None on failure. :rtype str or None """ try: filename = getpdb.get_pdb(pdb_id, source) except (RuntimeError, requests.HTTPError): msg = 'Could not obtain PDB file for {}'.format(pdb_id) if chain_name: msg += ' (chain %s)' % chain_name self.warning(msg) return False # getpdb.py module should be de-compressing GZ files automatically assert not filename.endswith('.gz') if chain_name: # Extract only the specified chain from the PDB file try: chain_st = extract_chain(filename, chain_name) except KeyError: self.warning(f'No chain "{chain_name}" found in PDB "{pdb_id}"') return False name, ext = fileutils.splitext(filename) filename = f"{name}_{chain_name}{ext}" chain_st.write(filename) if biological_unit: # Write separate files for each bilogical assembly. Original PDB # file is retained, but will not be imported into the PT. filename = create_biounits(filename) # Download diffraction data if diff_data: try: cv_file = getpdb.download_reflection_data(pdb_id) except (requests.HTTPError, FileNotFoundError) as err: msg = f'Could not find diffraction data for PDB: {pdb_id}' self.warning(msg) else: msg = "Downloaded diffraction data to: %s" % cv_file self.info(msg) return filename
[docs] def getPDB(self): """ Download the pdb file(s) with optional diffraction data. Return 0 for successful download else 1 :return: List of files written on success, or None on failure. :rtype: list(str) or None """ with self.manageDownloadFile(): written_files = self._getPDB() return written_files
[docs] def downloadFile(self): """ Download the pdb file of given PDB IDs by user. Sets the self.pdb_filepath to semicolon-separated string of downloaded file paths. """ multiple_paths = self.getPDB() if not multiple_paths: # error dialog was already shown return # This variable is used by getPdbDialog(): self.pdb_filepath = PATH_SEPARATOR.join(multiple_paths) self.accept()
[docs] def cancel(self): """ Reject the Pdb dialog. """ self.reject()
[docs] def exec_(self): """ Shows the dialog as a modal dialog, blocking until the user closes it. """ if not self.geometry.isEmpty(): self.restoreGeometry(self.geometry) ret = super(PDBDialog, self).exec_() self.geometry = self.saveGeometry() return ret
#Global instance of PDBDialog. pdb_instance = None
[docs]def getPdbDialog(): """ Called by maestro to bring up the dialog box. Returns string of downloaded filenames, separated by semi-colon. On cancel, returns emty string. """ global pdb_instance if pdb_instance is None: pdb_instance = PDBDialog() pdb_instance.pdb_filepath = "" pdb_instance.ui.pdb_id_text.setFocus() pdb_instance.ui.pdb_id_text.selectAll() pdb_instance.exec_() return pdb_instance.pdb_filepath
if __name__ == '__main__': print(__doc__)