From 796a89bd5b22a8e81a710f1e3c9641a4a1a06efb Mon Sep 17 00:00:00 2001 From: Jarrett Johnson Date: Thu, 14 Mar 2024 10:37:45 -0400 Subject: [PATCH] APBS electrostatics plugin --- data/startup/apbs_gui/__init__.py | 451 ++++++++ data/startup/apbs_gui/apbs.ui | 1405 +++++++++++++++++++++++ data/startup/apbs_gui/creating.py | 251 ++++ data/startup/apbs_gui/electrostatics.py | 195 ++++ data/startup/apbs_gui/qtwidgets.py | 32 + 5 files changed, 2334 insertions(+) create mode 100644 data/startup/apbs_gui/__init__.py create mode 100644 data/startup/apbs_gui/apbs.ui create mode 100644 data/startup/apbs_gui/creating.py create mode 100644 data/startup/apbs_gui/electrostatics.py create mode 100644 data/startup/apbs_gui/qtwidgets.py diff --git a/data/startup/apbs_gui/__init__.py b/data/startup/apbs_gui/__init__.py new file mode 100644 index 000000000..d7a242949 --- /dev/null +++ b/data/startup/apbs_gui/__init__.py @@ -0,0 +1,451 @@ +''' +PyMOL APBS GUI Plugin + +(c) Schrodinger, Inc. +''' + +import sys +import os +import importlib + +from pymol.Qt import QtCore, QtWidgets +from pymol.Qt.utils import loadUi, AsyncFunc, MainThreadCaller + +getOpenFileNames = QtWidgets.QFileDialog.getOpenFileNames + +from . import electrostatics +from .qtwidgets import ResizableMessageBox as QMessageBox + + +class SilentAbort(Exception): + pass + + +class StdOutCapture: + ''' +Redirect stdout and/or stderr to a temporary file until 'release' is called. + ''' + + def __init__(self, stdout=True, stderr=True): + import tempfile + self.temp = tempfile.TemporaryFile('w+') + self.save_stdout = None + self.save_stderr = None + if stdout: + self.save_stdout = sys.stdout + sys.stdout = self.temp + if stderr: + self.save_stderr = sys.stderr + sys.stderr = self.temp + + def release(self): + if self.save_stdout is not None: + sys.stdout = self.save_stdout + if self.save_stderr is not None: + sys.stderr = self.save_stderr + self.temp.seek(0) + content = self.temp.read() + self.temp.close() + return content + + +def run_impl(form, _self): + ''' +Execute the pipeline (prep, apbs, surface vis) + ''' + selection = form.input_sele.currentText() + prep_name = '' + map_name = '' + ramp_name = '' + group_name = [''] + + def get_name(lineinput): + name = lineinput.text() + if not name: + name = _self.get_unused_name(lineinput.placeholderText()) + return name + + def do_group(name): + if form.check_no_group.isChecked(): + return + if not group_name[0]: + group_name[0] = _self.get_unused_name('run', 1) + _self.group(group_name[0], name) + + if form.do_prepare.isChecked(): + from . import creating as pc + + prep_name = get_name(form.prep_name) + method = form.prep_method.currentText() + warnings = '' + + get_res_charge = lambda resn: ((-1) if resn in ('GLU', 'ASP') else + (1) if resn in ('ARG', 'LYS') else 0) + + def get_cb_pseudocharge(resn, name): + if name != 'CB': + return 0 + return get_res_charge(resn) + + try: + state = _self.get_selection_state(selection) + except BaseException as e: + state = -1 + + if method == 'pdb2pqr': + warnings = pc.pdb2pqr_cli(prep_name, selection, + options=form.pdb2pqr_args.text(), + quiet=0, + state=state, + preserve=form.check_preserve.isChecked(), + fixrna=form.pdb2pqr_fixrna.isChecked(), + _proclist=form._proclist, + exe=form.pdb2pqr_exe.text()) + if form.pdb2pqr_ignore_warnings.isChecked(): + warnings = '' + elif method == 'protein_assign_charges_and_radii': + _self.create(prep_name, selection) + _self.util.protein_assign_charges_and_radii(prep_name) + elif method.startswith('prepwizard'): + pc.prepwizard(prep_name, selection, + options=form.prepwizard_args.text(), + _proclist=form._proclist, + quiet=0, + preserve=form.check_preserve.isChecked()) + _self.alter(prep_name, 'elec_radius = vdw') + elif method.startswith('use formal'): + _self.create(prep_name, selection) + _self.alter(prep_name, + '(elec_radius, partial_charge) = (vdw, formal_charge)') + elif method.startswith('use CB'): + _self.create(prep_name, selection) + _self.alter( + prep_name, + '(elec_radius, partial_charge) = (vdw, getpc(resn, name))', + space={'getpc': get_cb_pseudocharge}) + elif method.startswith('use CA'): + _self.create(prep_name, '(%s) & name CA' % (selection)) + _self.alter( + prep_name, + '(elec_radius, vdw, partial_charge) = (3.0, 3.0, getpc(resn))', + space={'getpc': get_res_charge}) + elif method.startswith('use vdw'): + _self.create(prep_name, selection) + _self.alter(prep_name, 'elec_radius = vdw') + else: + raise ValueError('unknown method: ' + method) + + selection = prep_name + form.input_sele.addItem(prep_name) + do_group(prep_name) + + if warnings: + @form._callInMainThread + def result(): + msgbox = QMessageBox(QMessageBox.Question, 'Continue?', + method + ' emmitted warnings, do you want to continue?', + QMessageBox.Yes | QMessageBox.No , form._dialog) + msgbox.setDetailedText(warnings) + return msgbox.exec_() + + if result == QMessageBox.No: + raise SilentAbort + + if form.do_apbs.isChecked(): + map_name = get_name(form.apbs_map) + template = form.apbs_template.toPlainText() + electrostatics.map_new_apbs( + map_name, + selection, + grid=form.apbs_grid.value(), + focus=form.focus_sele.text(), + quiet=0, + preserve=form.check_preserve.isChecked(), + exe=form.apbs_exe.text(), + _template=template, + _proclist=form._proclist, + assign=0) + form.surf_map.addItem(map_name) + do_group(map_name) + + if form.do_surface.isChecked(): + if not map_name: + map_name = form.surf_map.currentText() + if not map_name: + raise ValueError('missing map') + + if not prep_name: + prep_name = _self.get_object_list(selection)[0] + + ramp_name = get_name(form.surf_ramp) + + v = form.surf_range.value() + _self.ramp_new(ramp_name, map_name, [-v, 0, v]) + do_group(ramp_name) + + sas = 'Accessible' in form.surf_type.currentText() + _self.set('surface_ramp_above_mode', not sas, prep_name) + _self.set('surface_solvent', sas, prep_name) + _self.set('surface_color', ramp_name, prep_name) + _self.show('surface', selection) + + +def dialog(_self=None): + if _self is None: + from pymol import cmd as _self + + dialog = QtWidgets.QDialog() + uifile = os.path.join(os.path.dirname(__file__), 'apbs.ui') + form = loadUi(uifile, dialog) + form._dialog = dialog + form._proclist = [] + + def set_apbs_in(contents): + form.apbs_template.setPlainText(contents.strip()) + + # hide options widgets + form.optarea_prep.setVisible(False) + form.optarea_apbs.setVisible(False) + form.optarea_surf.setVisible(False) + form.optarea_other.setVisible(False) + + # pre-fill form with likely data + names = _self.get_object_list() + names += ['(' + n + ')' for n in _self.get_names('public_selections')] + if names: + form.input_sele.clear() + form.input_sele.addItems([('polymer & ' + name) + if _self.count_atoms('polymer & ' + name) > 0 + else name for name in names]) + form.surf_map.addItems(_self.get_names_of_type('object:map')) + set_apbs_in(electrostatics.template_apbs_in) + + # executables + from shutil import which + form.apbs_exe.setText(electrostatics.find_apbs_exe() or 'apbs') + form.pdb2pqr_exe.setText( + which('pdb2pqr') or + which('pdb2pqr30') or + # acellera::htmd-pdb2pqr provides pdb2pqr_cli + which('pdb2pqr_cli') or 'pdb2pqr') + + + # for async panels + form._callInMainThread = MainThreadCaller() + run_impl_async = AsyncFunc(run_impl) + + # "Run" button callback + def run(): + form.tabWidget.setEnabled(False) + form.button_ok.clicked.disconnect() + form.button_ok.clicked.connect(abort) + form.button_ok.setText('Abort') + + form._capture = StdOutCapture() + + # detach from main thread + run_impl_async(form, _self) + + # "Run" button "finally" actions (main thread) + @run_impl_async.finished.connect + def run_finally(args): + _, exception = args + + form._proclist[:] = [] + stdout = form._capture.release() + print(stdout) + + form.button_ok.setText('Run') + form.button_ok.clicked.disconnect() + form.button_ok.clicked.connect(run) + form.button_ok.setEnabled(True) + form.tabWidget.setEnabled(True) + + if exception is not None: + handle_exception(exception, stdout) + return + + quit_msg = "Finished with Success. Close the APBS dialog?" + if QMessageBox.Yes == QMessageBox.question( + form._dialog, 'Finished', quit_msg, QMessageBox.Yes, + QMessageBox.No): + form._dialog.close() + + def handle_exception(e, stdout): + if isinstance(e, SilentAbort): + return + + msg = str(e) or 'unknown error' + msgbox = QMessageBox(QMessageBox.Critical, 'Error', + msg, QMessageBox.Close, form._dialog) + if stdout.strip(): + msgbox.setDetailedText(stdout) + msgbox.exec_() + + # "Abort" button callback + def abort(): + form.button_ok.setEnabled(False) + while form._proclist: + p = form._proclist.pop() + try: + p.terminate() + p.returncode = -15 # SIGTERM + except OSError as e: + print(e) + + # selection checker + check_sele_timer = QtCore.QTimer() + check_sele_timer.setSingleShot(True) + + # grid auto-value + form.apbs_grid_userchanged = False + form.apbs_grid.setStyleSheet('background: #ff6') + + @form.apbs_grid.editingFinished.connect + def _(): + form.apbs_grid_userchanged = True + form.apbs_grid.setStyleSheet('') + + @check_sele_timer.timeout.connect + def _(): + has_props = ['no', 'no'] + + def callback(partial_charge, elec_radius): + if partial_charge: has_props[0] = 'YES' + if elec_radius > 0: has_props[1] = 'YES' + + n = _self.iterate( + form.input_sele.currentText(), + 'callback(partial_charge, elec_radius)', + space={'callback': callback}) + + # grid auto-value (keep map size in the order of 200x200x200) + if n > 1 and not form.apbs_grid_userchanged: + e = _self.get_extent(form.input_sele.currentText()) + volume = (e[1][0] - e[0][0]) * (e[1][1] - e[0][1]) * (e[1][2] - e[0][2]) + grid = max(0.5, volume ** 0.333 / 200.0) + form.apbs_grid.setValue(grid) + + if n < 1: + label = 'Selection is invalid' + color = '#f66' + elif has_props == ['YES', 'YES']: + label = 'No preparation necessary, selection has charges and radii' + form.do_prepare.setChecked(False) + color = '#6f6' + else: + label = 'Selection needs preparation (partial_charge: %s, elec_radius: %s)' % tuple( + has_props) + form.do_prepare.setChecked(True) + color = '#fc6' + + form.label_sele_has.setText(label) + form.label_sele_has.setStyleSheet('background: %s; padding: 5' % color) + + check_sele_timer.start(0) + + @form.apbs_exe_browse.clicked.connect + def _(): + fnames = getOpenFileNames(None, filter='apbs (apbs*);;All Files (*)')[0] + if fnames: + form.apbs_exe.setText(fnames[0]) + + @form.pdb2pqr_exe_browse.clicked.connect + def _(): + fnames = getOpenFileNames(None, filter='pdb2pqr (pdb2pqr*);;All Files (*)')[0] + if fnames: + form.pdb2pqr_exe.setText(fnames[0]) + + # hook up events + form.input_sele.currentIndexChanged.connect( + lambda: check_sele_timer.start(0)) + form.input_sele.editTextChanged.connect( + lambda: check_sele_timer.start(1000)) + + form.button_ok.clicked.connect(run) + + # "Register" opens a web browser + @form.button_register.clicked.connect + def _(): + import webbrowser + webbrowser.open("http://www.poissonboltzmann.org/") + + @form.button_load.clicked.connect + def _(): + fnames = getOpenFileNames( + None, filter='APBS Input (*.in);;All Files (*)')[0] + if fnames: + contents = load_apbs_in(form, fnames[0]) + set_apbs_in(contents) + + @form.button_reset.clicked.connect + def _(): + set_apbs_in(electrostatics.template_apbs_in) + + form._dialog.show() + form._dialog.resize(500, 600) + + +def load_apbs_in(form, filename, contents=''): + import shlex + from pymol import cmd, importing + + wdir = os.path.dirname(filename) + + if not contents: + contents = cmd.file_read(filename) + + if not isinstance(contents, str): + contents = contents.decode() + + sectionkeys = ('read', 'elec', 'apolar', 'print') + section = '' + insert_write_pot = True + + lines = [] + + for line in contents.splitlines(): + a = shlex.split(line) + key = a[0].lower() if a else '' + + if not section: + if key in sectionkeys: + section = key + elif key == 'end': + if section == 'elec' and insert_write_pot: + lines.append('write pot dx "{mapfile}"') + section = '' + elif section == 'read': + if len(a) > 2 and key in ('charge', 'kappa', 'mol', 'parm', 'pot'): + filename = os.path.join(wdir, a[2]) + if os.path.exists(filename): + format = a[1].lower() + if key == 'mol' and format in ('pqr', 'pdb'): + # load into PyMOL and update selection dropdown + oname = importing.filename_to_objectname(a[2]) + oname = cmd.get_unused_name(oname, 0) + cmd.load(filename, oname, format=format) + form.input_sele.addItem(oname) + form.input_sele.setEditText(oname) + + # absolute path in input file + a[2] = '"' + filename + '"' + line = ' '.join(a) + else: + QMessageBox.warning( + form._dialog, "Warning", + f'Warning: File "{filename}" does not exist') + + elif section == 'elec': + if key == 'write': + if a[1:4] == ['pot', 'dx', "{mapfile}"]: + insert_write_pot = False + + lines.append(line) + + return '\n'.join(lines) + + +def __init_plugin__(app=None): + from pymol.plugins import addmenuitemqt as addmenuitem + addmenuitem('APBS Electrostatics', dialog) diff --git a/data/startup/apbs_gui/apbs.ui b/data/startup/apbs_gui/apbs.ui new file mode 100644 index 000000000..5459ef440 --- /dev/null +++ b/data/startup/apbs_gui/apbs.ui @@ -0,0 +1,1405 @@ + + + Form + + + + 0 + 0 + 530 + 654 + + + + + 200 + 200 + + + + APBS Electrostatics + + + + + + 0 + + + + + 0 + 0 + + + + QWidget[transparent_background=true] {background-color:transparent} + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOff + + + true + + + true + + + Main + + + + + 0 + 0 + 485 + 965 + + + + true + + + + 10 + + + 12 + + + 10 + + + 10 + + + + + + + Selection: + + + input_sele + + + + + + + + 0 + 0 + + + + true + + + + polymer + + + + + + + + + + + true + + + + background: #f66; padding: 5; + + + Selection has ... + + + + + + + QFrame::StyledPanel + + + + + + + + uncheck if the selected molecule already has partial charges and radii ("elec_radius" property) assigned, e.g. if you have loaded a PQR file + + + Prepare Molecule + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Options >> + + + true + + + + + + + + + true + + + + 0 + 0 + + + + + QLayout::SetDefaultConstraint + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Method: + + + prep_method + + + + + + + + pdb2pqr + + + + + prepwizard (SCHRODINGER) + + + + + protein_assign_charges_and_radii + + + + + use formal_charge and vdw + + + + + use CB-pseudocharge and vdw + + + + + use CA-pseudocharge and radius=3.0 + + + + + use vdw + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + prepared + + + + + + + Output Molecule Object: + + + prep_name + + + + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + "pdb2pqr" adds hydrogens and missing sidechain atoms, assigns partial charges and radii. REMOVES ligands and modified residues. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + Ignore warnings + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + "prepwizard" adds hydrogens and missing sidechain atoms, and assigns partial charges. Can handle ligands and modified residues. PyMOL's vdw radii will be used. Requires Schrodinger Suite. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + Additional command line options for prepwizard, for example "-r 2.0" or "-fix" + + + extra command line options + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + "protein_assign_charges_and_radii" REMOVES incomplete or modified residues, adds missing C-terminus, and assigns AMBER99 partial charges and radii + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Coarse grained charge model: use formal charges (-1/0/1) and PyMOL's vdw radii. Note that missing sidechains of charged residues (e.g. GLU) will not contribute any charge! + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Coarse grained charge model for proteins with "stub" side chains: place a pseudo charge on the CB atom of GLU, ASP, ARG and LYS + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Coarse grained charge model for CA-only models: place a pseudo charge on the CA atom of GLU, ASP, ARG and LYS and set a radius of 3.0 for all atoms + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Use existing "partial_charge", use vdw as "elec_radius" + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + + + + + + + + + QFrame::StyledPanel + + + + + + + + Calculate Map with APBS + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Options >> + + + true + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + limit the expensive fine grid calculation to a region of interest, e.g. a binding pocket + + + Focus Selection (optional): + + + focus_sele + + + + + + + + + + Output Map Object: + + + apbs_map + + + + + + + + + + apbs_map + + + + + + + Grid spacing not guaranteed, will increase grid spacing if grid doesn't fit into memory + + + Grid Spacing: + + + apbs_grid + + + + + + + Angstrom + + + 0.010000000000000 + + + 0.250000000000000 + + + 0.500000000000000 + + + + + + + + + + + + + QFrame::StyledPanel + + + + + + + + Molecular Surface Visualization + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Options >> + + + true + + + false + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Map: + + + surf_map + + + + + + + false + + + true + + + + + + + Range: +/- + + + surf_range + + + + + + + 5.000000000000000 + + + + + + + Output Ramp: + + + surf_ramp + + + + + + + + + + apbs_ramp + + + + + + + + Solvent Excluded Surface (Connolly surface) + + + + + Solvent Accessible Surface + + + + + + + + + true + + + + Projects the electrostatic potential onto the molecular surface + + + + + + + + + + + + + QFrame::StyledPanel + + + + + + + + Other Visualizations + + + optbutton_other + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Options >> + + + true + + + false + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + true + + + + After the map has been calculated, create additional visualizations using the "Action" items in the object menu panel: + + + true + + + + + + + Isosurface with "A > surface > level +/-1.0" + + + 10 + + + + + + + Volume with "A > volume > esp" + + + 10 + + + + + + + Field lines with "A > gradient > default" + + + 10 + + + + + + + Slice with "A > slice > default" + + + 10 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + APBS Template + + + + 10 + + + 12 + + + 10 + + + 10 + + + + + Documentation: <a href="https://apbs.readthedocs.io/en/latest/using/input/elec/">apbs.readthedocs.io</a> + + + true + + + + + + + + + Load existing "apbs.in" file: + + + + + + + Browse... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Reset + + + + + + + + + + Monospace + + + + QPlainTextEdit::NoWrap + + + + + + + + + + + Advanced Configuration + + + + 10 + + + 12 + + + 10 + + + 10 + + + + + Program Locations + + + + + + apbs + + + apbs_exe + + + + + + + + + + ... + + + + + + + pdb2pqr + + + pdb2pqr_exe + + + + + + + + + + ... + + + + + + + + + + pdb2pqr + + + + + + command line options: + + + pdb2pqr_args + + + + + + + --ff=AMBER + + + + + + + For RNA, use residue names RA, RC, RG, RU + + + true + + + + + + + + + + Objects and Files + + + + + + Don't group new objects + + + + + + + Don't delete temporary files + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Register APBS Use + + + + + + + Run + + + + + + + + + input_sele + do_prepare + prep_method + prep_name + do_apbs + focus_sele + apbs_grid + apbs_map + do_surface + surf_map + surf_range + surf_ramp + surf_type + button_register + button_ok + tabWidget + apbs_exe + apbs_exe_browse + pdb2pqr_exe + pdb2pqr_exe_browse + pdb2pqr_args + + + + + optbutton_surface + toggled(bool) + optarea_surf + setVisible(bool) + + + 484 + 206 + + + 368 + 261 + + + + + do_surface + toggled(bool) + optarea_surf + setEnabled(bool) + + + 268 + 210 + + + 269 + 261 + + + + + optbutton_apbs + toggled(bool) + optarea_apbs + setVisible(bool) + + + 484 + 51 + + + 320 + 94 + + + + + do_apbs + toggled(bool) + optarea_apbs + setEnabled(bool) + + + 229 + 53 + + + 145 + 94 + + + + + optbutton_prepare + toggled(bool) + optarea_prep + setVisible(bool) + + + 484 + -155 + + + 325 + -89 + + + + + do_prepare + toggled(bool) + optarea_prep + setEnabled(bool) + + + 157 + -163 + + + 128 + -90 + + + + + apbs_map + textChanged(QString) + surf_map + setEditText(QString) + + + 442 + 163 + + + 442 + 297 + + + + + do_apbs + toggled(bool) + surf_map + setDisabled(bool) + + + 229 + 53 + + + 442 + 297 + + + + + optbutton_other + toggled(bool) + optarea_other + setVisible(bool) + + + 484 + 435 + + + 429 + 502 + + + + + prep_method + currentIndexChanged(int) + stackedWidget + setCurrentIndex(int) + + + 401 + -119 + + + 199 + -38 + + + + + diff --git a/data/startup/apbs_gui/creating.py b/data/startup/apbs_gui/creating.py new file mode 100644 index 000000000..6b3bb37e3 --- /dev/null +++ b/data/startup/apbs_gui/creating.py @@ -0,0 +1,251 @@ +''' +(c) 2010-2012 Thomas Holder (https://github.com/speleo3/pymol-psico) +(c) 2016 Thomas Holder, Schrodinger, Inc. + +License: BSD-2-Clause +''' + +from pymol import cmd, CmdException + + +def pdb2pqr(name, selection='all', ff='amber', debump=1, opt=1, assignonly=0, + ffout='', ph=None, neutraln=0, neutralc=0, state=-1, preserve=0, + exe='pdb2pqr', quiet=1): + ''' +DESCRIPTION + + Creates a new molecule object from a selection and adds missing atoms, + assignes charges and radii using PDB2PQR. + + http://www.poissonboltzmann.org/pdb2pqr/ + +USAGE + + pdb2pqr name [, selection [, ff [, debump [, opt [, assignonly [, ffout [, + ph [, neutraln [, neutralc [, state [, preserve ]]]]]]]]]]] + +ARGUMENTS + + name = string: name of object to create or modify + + selection = string: atoms to include in the new object {default: all} + + ff = string: forcefield {default: amber} + ''' + debump, opt, assignonly = int(debump), int(opt), int(assignonly) + neutraln, neutralc = int(neutraln), int(neutralc) + quiet = int(quiet) + + args = ['--ff=' + ff, '--chain'] + if not debump: + args.append('--nodebump') + if not opt: + args.append('--noopt') + if assignonly: + args.append('--assign-only') + if ffout: + args.append('--ffout=' + ffout) + if ph is not None: + args.append('--with-ph=%f' % float(ph)) + if neutraln: + args.append('--neutraln') + if neutralc: + args.append('--neutralc') + if not quiet: + args.append('--verbose') + + r = pdb2pqr_cli(name, selection, args, state, preserve, exe, quiet) + + if not quiet: + if r: + print(r) + + print(' pdb2pqr: done') + + return r + + +def pdb2pqr_cli(name, selection, options, state=-1, preserve=0, + exe='pdb2pqr', quiet=1, fixrna=0, _proclist=None): + import os, tempfile, subprocess, shutil + + state, preserve, quiet = int(state), int(preserve), int(quiet) + + if cmd.is_string(options): + import shlex + options = shlex.split(options) + + args = [cmd.exp_path(exe)] + list(options) + + tmpdir = tempfile.mkdtemp() + # Input format should be PDB, but use PQR instead because we can support + # multi-state assemblies by not writing MODEL records. + infile = os.path.join(tmpdir, 'in.pqr') + outfile = os.path.join(tmpdir, 'out.pqr') + args.extend([infile, outfile]) + + # For some reason, catching stdout/stderr with PIPE and communicate() + # blocks terminate() calls from terminating early. Using a file + # redirect doesn't show this problem. + f_stdout = open(os.path.join(tmpdir, 'stdout.txt'), 'w+') + + # RNA resdiue names must be RA, RC, RG, and RU + tmpmodel = '' + if int(fixrna) and cmd.count_atoms('(%s) & resn A+C+G+U' % (selection)): + tmpmodel = cmd.get_unused_name('_tmp') + cmd.create(tmpmodel, selection, zoom=0) + cmd.alter(tmpmodel + ' & polymer.nucleic & resn A+C+G+U', + 'resn = "R" + resn') + selection = tmpmodel + + try: + cmd.save(infile, selection, state) + + p = subprocess.Popen(args, cwd=tmpdir, + stdin=subprocess.PIPE, # Windows pythonw fix + stdout=f_stdout, + stderr=f_stdout) + p.stdin.close() # Windows pythonw fix + + if _proclist is not None: + _proclist.append(p) + + p.wait() + + # This allows PyMOL to capture it and display the output in the GUI. + f_stdout.seek(0) + print(f_stdout.read().rstrip()) + + if p.returncode == -15: # SIGTERM + raise CmdException('pdb2pqr terminated') + + if p.returncode != 0: + raise CmdException('%s failed with exit status %d' % + (args[0], p.returncode)) + + warnings = [L[10:] for L in open(outfile) if L.startswith('REMARK 5')] + warnings = ''.join(warnings).strip() + + cmd.load(outfile, name) + + return warnings + + except OSError as e: + print(e) + raise CmdException('Cannot execute "%s"' % (exe)) + finally: + if tmpmodel: + cmd.delete(tmpmodel) + + f_stdout.close() + + if not preserve: + shutil.rmtree(tmpdir) + elif not quiet: + print(' Notice: not deleting', tmpdir) + + +def _is_exe(exe): + import os, sys + if os.path.exists(exe): + return True + if sys.platform.startswith('win'): + return os.path.exists(exe + '.exe') + return False + + +def prepwizard(name, selection='all', options='', state=-1, + preserve=0, exe='$SCHRODINGER/utilities/prepwizard', quiet=1, + _proclist=None): + ''' +DESCRIPTION + + Run the SCHRODINGER Protein Preparation Wizard. Builds missing side + chains and converts MSE to MET. Other non-default options need to be + passed with the "options=" argument. + +USAGE + + prepwizard name [, selection [, options [, state ]]] + +ARGUMENTS + + name = str: name of object to create + + selection = str: atoms to send to the wizard {default: all} + + options = str: additional command line options for prepwizard + + state = int: object state {default: -1 (current)} + ''' + import os, tempfile, subprocess, shutil, shlex + + state, preserve, quiet = int(state), int(preserve), int(quiet) + + exe = cmd.exp_path(exe) + if not _is_exe(exe): + if 'SCHRODINGER' not in os.environ: + print(' Warning: SCHRODINGER environment variable not set') + raise CmdException('no such script: ' + exe) + + args = [exe, '-mse', '-fillsidechains', '-WAIT'] + + if options: + if cmd.is_string(options): + options = shlex.split(options) + args.extend(options) + + tmpdir = tempfile.mkdtemp() + infile = 'in.pdb' + outfile = 'out.mae' + args.extend([infile, outfile]) + + try: + cmd.save(os.path.join(tmpdir, infile), selection, state) + + env = dict(os.environ) + env.pop('PYTHONEXECUTABLE', '') # messes up Python on Mac + + import pymol + if pymol.IS_WINDOWS: + # Fix for 2020-4 (PYMOL-3572) + import ctypes + ctypes.windll.kernel32.SetDllDirectoryW(None) + + p = subprocess.Popen(args, cwd=tmpdir, + env=env, + universal_newlines=True, + stdin=subprocess.PIPE, # Windows pythonw fix + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + if _proclist is not None: + _proclist.append(p) + + print(p.communicate()[0].rstrip()) + + if p.wait() == -15: # SIGTERM + raise CmdException('prepwizard terminated') + + if p.returncode != 0: + raise CmdException('%s failed with exit status %d' % (args[0], p.returncode)) + + cmd.load(os.path.join(tmpdir, outfile), name) + except OSError as e: + print(e) + raise CmdException('Cannot execute "%s"' % (exe)) + finally: + logfile = os.path.join(tmpdir, 'in.log') + if os.path.exists(logfile): + with open(logfile) as handle: + print(handle.read()) + if not preserve: + shutil.rmtree(tmpdir) + elif not quiet: + print(' Notice: not deleting', tmpdir) + + if not quiet: + print(' prepwizard: done') + + +# vi: ts=4:sw=4:smarttab:expandtab diff --git a/data/startup/apbs_gui/electrostatics.py b/data/startup/apbs_gui/electrostatics.py new file mode 100644 index 000000000..dc491ce9f --- /dev/null +++ b/data/startup/apbs_gui/electrostatics.py @@ -0,0 +1,195 @@ +''' +APBS wrapper + +(c) 2012 Thomas Holder (https://github.com/speleo3/pymol-psico) +(c) 2016 Thomas Holder, Schrodinger, Inc. + +License: BSD-2-Clause +''' + +import os + +from pymol import cmd, CmdException + +template_apbs_in = ''' +read + mol pqr "{pqrfile}" +end +elec + mg-auto + mol 1 + + fgcent {fgcent} # fine grid center + cgcent mol 1 # coarse grid center + fglen {fglen} + cglen {cglen} + dime {dime} + lpbe # l=linear, n=non-linear Poisson-Boltzmann equation + bcfl sdh # "Single Debye-Hueckel" boundary condition + pdie 2.0 # protein dielectric + sdie 78.0 # solvent dielectric + chgm spl2 # Cubic B-spline discretization of point charges on grid + srfm smol # smoothed surface for dielectric and ion-accessibility coefficients + swin 0.3 + temp 310.0 # temperature + sdens 10.0 + calcenergy no + calcforce no + srad {srad} # solvent radius + + ion charge +1 conc 0.15 radius 2.0 + ion charge -1 conc 0.15 radius 1.8 + + write pot dx "{mapfile}" +end +quit +''' + + +def find_apbs_exe(): + import shutil + exe = shutil.which('apbs') + if not exe: + exe = cmd.exp_path('$SCHRODINGER/utilities/apbs') + if not os.path.exists(exe): + return None + return exe + + +def validate_apbs_exe(exe): + '''Get and validate apbs executable. + Raise CmdException if not found or broken.''' + import subprocess + + if exe: + exe = cmd.exp_path(exe) + else: + exe = find_apbs_exe() or 'apbs' + + try: + r = subprocess.call([exe, "--version"], + stdin=subprocess.PIPE, # Windows pythonw fix + stdout=open(os.devnull, "w"), stderr=subprocess.STDOUT) + if r < 0: + raise CmdException("Broken executable: " + exe) + except OSError as e: + print(e) + raise CmdException("Cannot execute: " + exe) + + return exe + +def map_new_apbs(name, selection='all', grid=0.5, buffer=10.0, state=1, + preserve=0, exe='', assign=0, focus='', quiet=1, _template='', + _proclist=None): + ''' +DESCRIPTION + + Create electrostatic potential map with APBS. + + "selection" needs partial charges (partial_charge) and radii (elec_radius) + assigned. These can be loaded for example from a PQR file. + +SEE ALSO + + map_new (coulomb), psico.electrostatics + ''' + import tempfile, shutil, glob, subprocess + + selection = '(%s) and not solvent' % (selection) + grid, buffer, state = float(grid), float(buffer), int(state) + preserve, assign, quiet = int(preserve), int(assign), int(quiet) + + # path to apbs executable + exe = validate_apbs_exe(exe) + + # temporary directory + tempdir = tempfile.mkdtemp() + if not quiet: + print(' Tempdir:', tempdir) + + # filenames + pqrfile = os.path.join(tempdir, 'mol.pqr') + infile = os.path.join(tempdir, 'apbs.in') + stem = os.path.join(tempdir, 'map') + + # temporary selection + tmpname = cmd.get_unused_name('_sele') + cmd.select(tmpname, selection, 0) + + cmd.save(pqrfile, tmpname, state, format='pqr', quiet=quiet) + + # grid dimensions + extent = cmd.get_extent(tmpname, state) + extentfocus = cmd.get_extent(focus) if focus else extent + fglen = [(emax-emin + 2*buffer) for (emin, emax) in zip(*extentfocus)] + cglen = [(emax-emin + 4*buffer) for (emin, emax) in zip(*extent)] + # make coarse grid a cube (better for non-globular shapes) + cglen = [max(cglen)] * 3 + + cmd.delete(tmpname) + + apbs_in = { + 'pqrfile': pqrfile, + 'fgcent': 'mol 1', + 'fglen': '%f %f %f' % tuple(fglen), + 'cglen': '%f %f %f' % tuple(cglen), + 'srad': cmd.get('solvent_radius'), + 'mapfile': stem, + } + + if focus: + apbs_in['fgcent'] = '%f %f %f' % tuple((emax + emin) / 2.0 + for (emin, emax) in zip(*extentfocus)) + + try: + # apbs will fail if grid does not fit into memory + # -> on failure repeat with larger grid spacing + for _ in range(3): + dime = [1 + max(64, n / grid) for n in fglen] + apbs_in['dime'] = '%d %d %d' % tuple(dime) + + # apbs input file + with open(infile, 'w') as f: + f.write((_template or template_apbs_in).format(**apbs_in)) + + # run apbs + p = subprocess.Popen([exe, infile], cwd=tempdir) + + if _proclist is not None: + _proclist.append(p) + + r = p.wait() + + if r == -15: # SIGTERM + raise CmdException('apbs terminated') + + if r == 0: + break + + if r in (-6, -9): + grid *= 2.0 + if not quiet: + print(' Warning: retry with grid =', grid) + continue + + raise CmdException('apbs failed with code ' + str(r)) + + dx_list = glob.glob(stem + '*.dx') + if not dx_list: + dx_list = glob.glob(stem + '*.dxbin') + if len(dx_list) != 1: + raise CmdException('dx file missing') + + # load map + cmd.load(dx_list[0], name, quiet=quiet) + except OSError as e: + print(e) + raise CmdException('Cannot execute "%s"' % (exe)) + finally: + if not preserve: + shutil.rmtree(tempdir) + elif not quiet: + print(' Notice: not deleting %s' % (tempdir)) + + +# vi: ts=4:sw=4:smarttab:expandtab diff --git a/data/startup/apbs_gui/qtwidgets.py b/data/startup/apbs_gui/qtwidgets.py new file mode 100644 index 000000000..a037c08ee --- /dev/null +++ b/data/startup/apbs_gui/qtwidgets.py @@ -0,0 +1,32 @@ +from pymol.Qt import QtCore, QtWidgets + + +class ResizableMessageBox(QtWidgets.QMessageBox): + + _EVENT_TYPES = ( + QtCore.QEvent.UpdateRequest, + QtCore.QEvent.WinIdChange, + QtCore.QEvent.ShowToParent, + ) + + _UNWANTED_WINDOW_FLAGS = ( + QtCore.Qt.MSWindowsFixedSizeDialogHint | + 0) + + def _make_resizable(self): + textEdit = self.findChild(QtWidgets.QTextEdit) + if textEdit is None: + return + + self.setSizeGripEnabled(True) + + ex = QtWidgets.QSizePolicy.Expanding + for w in [self, textEdit]: + w.setMaximumSize(0xffffff, 0xffffff) + w.setSizePolicy(ex, ex) + + def event(self, e): + if e.type() in self._EVENT_TYPES: + self._make_resizable() + + return super(ResizableMessageBox, self).event(e)