Source code for schrodinger.ui.qt.pdb_dialog

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

import inflect
import os
from contextlib import contextmanager

import requests

from schrodinger import get_maestro
from schrodinger import structure
from schrodinger.protein import biounit
from schrodinger.protein import getpdb
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import widgetmixins
from schrodinger.utils import fileutils
from schrodinger.ui.qt import mapperwidgets
from schrodinger.utils.documentation import show_topic

from . import pdb_dialog_ui
from . import stylesheet

PATH_SEPARATOR = ';'

REMOTE_FETCH_KEY = 'maestro_remote_fetch'

maestro = get_maestro()


[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, import_pdbs=False): QtWidgets.QDialog.__init__(self, None) self.import_pdbs = import_pdbs 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) self.auto_fetch = QtGui.QAction("Local or Web") self.retrieve_from_local = QtGui.QAction("Local Installation Only") self.download_from_web = QtGui.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) item.setCheckable(True) self.btn_group = mapperwidgets.MappableActionGroup({ self.auto_fetch: True, self.retrieve_from_local: False, self.download_from_web: False, }) 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
[docs] def execRemoteQueryDialog(self) -> bool: """ Launches the relevant remote query dialog if applicable. :return: if the remote dialog is accepted """ # TODO: see MAE-45812 return True
def _getPDB(self): """ Retrieve PDB file(s) specified in the UI. Returns dictionary of files keyed on PDB Id. :return: dictionary that has PDB Ids as keys and file paths as values :rtype: dict """ biological_unit = self.ui.biological_unit.isChecked() search_auto = self.auto_fetch.isChecked() local_only = self.retrieve_from_local.isChecked() remote_only = self.download_from_web.isChecked() if remote_only: source = getpdb.WEB else: # if auto fetch is on, it should search local before remote source = getpdb.DATABASE 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 pdb_dict = {} error_ids = [] dialog_shown = False remote_ok = False for pdb_id in pdb_id_list: pdb_file = self._downloadPdb(source, pdb_id, chain_name, biological_unit) if not pdb_file and search_auto: if not dialog_shown: remote_ok = self.execRemoteQueryDialog() dialog_shown = True if remote_ok: pdb_file = self._downloadPdb(getpdb.WEB, pdb_id, chain_name, biological_unit) if pdb_file: pdb_dict[pdb_id] = pdb_file else: error_ids.append(pdb_id) if error_ids: error_id_str = ', '.join(error_ids) if chain_name: error_id_str += f' (chain {chain_name})' if local_only: title = 'Get PDB - No Results on Local Server' text = ( f'<p>{error_id_str} {inflect.engine().plural_verb("is", len(error_ids))}' f' not available on your local server.</p>' f'<p>To use a remote server, change the "Local or Web" option,' f' available from the <i>Change</i> menu button on the ' f'<i>Get PDB</i> dialog.</p>') self.showMessageBox(text=text, title=title) else: msg = ( f'Could not obtain PDB {inflect.engine().plural_noun("file", len(error_ids))}' f' for {error_id_str}') self.warning(msg) return pdb_dict def _downloadPdb(self, source, pdb_id, chain_name, biological_unit): """ 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 :return: Returns the file path to the written file, or False on failure. :rtype: str or bool """ diff_data = self.ui.diffraction_data.isChecked() try: if source == getpdb.WEB and diff_data: # EM maps are supported for CIF files only filename = getpdb.download_cif(pdb_id) else: # NOTE: This will also download the CIF file if structure is # too big to fit into a PDB file. filename = getpdb.get_pdb(pdb_id, source) except (RuntimeError, requests.HTTPError, requests.ConnectionError): 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 biological assembly. Original PDB # file is retained, but will not be imported into the PT. filename = create_biounits(filename) return filename def _downloadMaps(self, pdb_id, emdb_ids): """ Download diffraction and EM data for a given PDB ID and EMDB codes and return path to written EM file(s). :param pdb_id: PDB ID :type pdb_id: str :param emdb_ids: list of related EMDB ids :type emdb_ids: List[str] :return: Returns list of written EM file paths. :rtype: List[str] """ no_diff_data = False try: cv_file = getpdb.download_reflection_data(pdb_id) except (requests.HTTPError, requests.ConnectionError, FileNotFoundError): no_diff_data = True else: msg = "Downloaded diffraction data to: %s" % cv_file self.info(msg) em_files = [] no_em_data = True if emdb_ids: for _code in emdb_ids: try: em_file = getpdb.download_em_map(_code) except (requests.HTTPError, requests.ConnectionError, FileNotFoundError): msg = f'Failed to download EM map for EMD-{_code}' self.warning(msg) else: em_files.append(em_file) no_em_data = False # Show warning if neither diffraction data nor EM maps were found. if no_diff_data and not em_files: msg = f'Could not find diffraction data or EM map for PDB: {pdb_id}' self.warning(msg) return em_files
[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(s) of given PDB ID(s) by user. If diffraction data is requested tries to download diffraction data files and EM maps. If EM maps are found their surfaces are added to corresponding PDB entries in the Project Table. """ diff_data = self.ui.diffraction_data.isChecked() pdb_dict = self.getPDB() if not pdb_dict: # error dialog was already shown return # When PDBs are not imported just save PDB files. This is needed, # for example, in MSV GUI, which has special methods to load PDBs. if not self.import_pdbs: self.pdb_filepath = PATH_SEPARATOR.join(pdb_dict.values()) self.accept() return # Load PDBs to Maestro Project Table pt = maestro.project_table_get() for pdb_id, pdb_file in pdb_dict.items(): # Import PDB structure to the Project Table pt.importStructureFile(pdb_file, wsreplace=True) if not diff_data: continue # Download diffraction data and EM Maps if they are available. entry_id = pt.last_added_entry.entry_id st = pt.last_added_entry.getStructure() emdb_ids = self._getEMDBIds(st) em_files = self._downloadMaps(pdb_id, emdb_ids) # Check whether EM maps are available if em_files: self._importEMFiles(entry_id, em_files) self.accept()
def _importEMFiles(self, entry_id, em_files): """ Imports surfaces from given EM maps and associate them with a given Project Table entry. :param entry_id: entry Id that imported surfaces should be associated with :type entry_id: int :param em_files: list of EM files :type em_files: List[str] """ for _em_file in em_files: _em_name, _ = os.path.splitext(os.path.basename(_em_file)) _surface_name = _em_name.capitalize() maestro.command('visimport entry=%s isovalue=5.0 "%s":::%s' % (entry_id, _em_file, _surface_name)) maestro.command('surfacesetisovalue entry=%s isovalue=3.0 "%s"' % (entry_id, _surface_name)) def _getEMDBIds(self, st): """ Get EMDB Ids from the properties stored in a given structure. :param st: structure object :type st: structure.Structure :return: list of EMDB Ids :rtype: List[str] """ emdb_ids = [] icnt = 1 while True: db_name_prop = f's_pdb_PDB_REMARK_900_Related_Entry_{icnt}_db_name' db_name = st.property.get(db_name_prop, None) if db_name is None: break if db_name.startswith('EMDB'): db_id_prop = f's_pdb_PDB_REMARK_900_Related_Entry_{icnt}_db_id' db_id = st.property.get(db_id_prop, None) if db_id and db_id.startswith('EMD-'): emdb_ids.append(db_id[4:]) icnt += 1 return emdb_ids
[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 empty string. """ global pdb_instance if pdb_instance is None: pdb_instance = PDBDialog(import_pdbs=True) pdb_instance.pdb_filepath = "" pdb_instance.ui.pdb_id_text.setFocus() pdb_instance.ui.pdb_id_text.selectAll() pdb_instance.exec() return True
if __name__ == '__main__': print(__doc__)