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 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] @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__)