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