From 795e664d7f2df1b72fe7f33e7a6a32b13845f2a7 Mon Sep 17 00:00:00 2001 From: Jarrett Johnson Date: Wed, 28 Feb 2024 14:12:54 -0500 Subject: [PATCH 1/6] PYMOL-4787: Bind Global Texture before selection indicator write --- layer0/Texture.cpp | 8 ++++++++ layer0/Texture.h | 17 +++++++++++++++++ layer3/Executive.cpp | 4 ++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/layer0/Texture.cpp b/layer0/Texture.cpp index 8e00f2000..02868bcd1 100644 --- a/layer0/Texture.cpp +++ b/layer0/Texture.cpp @@ -260,6 +260,14 @@ void TextureGetPlacementForNewSubtexture(PyMOLGlobals * G, int new_texture_width I->xpos += new_texture_width + 1; // added space for running on Ipad/Iphone (weird artifacts) } +void TextureFillNewSubtexture(PyMOLGlobals* G, int width, int height, int x, int y, const void* data) +{ + CTexture *I = G->Texture; + if (I->texture) { + I->texture->texture_subdata_2D(x, y, width, height, data); + } +} + void TextureFree(PyMOLGlobals * G) { /* TODO -- free all the resident textures */ diff --git a/layer0/Texture.h b/layer0/Texture.h index c920a2bbb..d6b58f2eb 100644 --- a/layer0/Texture.h +++ b/layer0/Texture.h @@ -34,7 +34,24 @@ void TextureInitTextTexture(PyMOLGlobals * G); * Binds the global Text Texture */ void TextureBindTexture(PyMOLGlobals* G); + +/** + * Allocates a section of the global texture for a new subtexture. + * @param new_texture_width width of the new subtexture + * @param new_texture_height height of the new subtexture + * @param[out] new_texture_posx x position of the new subtexture + * @param[out] new_texture_posy y position of the new subtexture + */ void TextureGetPlacementForNewSubtexture(PyMOLGlobals * G, int new_texture_width, int new_texture_height, int *new_texture_posx, int *new_texture_posy); + +/** + * Fills a new subtexture with the given data. + * @param width width of the new subtexture + * @param height height of the new subtexture + * @param x x position of the new subtexture + * @param y y position of the new subtexture + */ +void TextureFillNewSubtexture(PyMOLGlobals* G, int width, int height, int x, int y, const void* data); int TextureGetTextTextureSize(PyMOLGlobals * G); #endif diff --git a/layer3/Executive.cpp b/layer3/Executive.cpp index 3edc886bd..4a8371166 100644 --- a/layer3/Executive.cpp +++ b/layer3/Executive.cpp @@ -8056,8 +8056,8 @@ static void ExecutiveRegenerateTextureForSelector(PyMOLGlobals *G, int round_poi // printf("\n"); } } - glTexSubImage2D(GL_TEXTURE_2D, 0, I->selectorTexturePosX, I->selectorTexturePosY, - widths_arg[0], widths_arg[0], GL_RGBA, GL_UNSIGNED_BYTE, temp_buffer); + TextureFillNewSubtexture(G, widths_arg[0], widths_arg[0], + I->selectorTexturePosX, I->selectorTexturePosY, temp_buffer); FreeP(temp_buffer); } From 545e59418a87a9045d1bc84297d5ff9ba83e96d1 Mon Sep 17 00:00:00 2001 From: Thomas Holder Date: Fri, 15 Mar 2024 13:26:43 +0100 Subject: [PATCH 2/6] Fix colors panel (#345) Sliders need `int` value. --- modules/pmg_qt/pymol_qt_gui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/pmg_qt/pymol_qt_gui.py b/modules/pmg_qt/pymol_qt_gui.py index a0714ea66..f9be5f3df 100644 --- a/modules/pmg_qt/pymol_qt_gui.py +++ b/modules/pmg_qt/pymol_qt_gui.py @@ -573,9 +573,9 @@ def update_gui(*args): R = form.input_R.value() G = form.input_G.value() B = form.input_B.value() - form.slider_R.setValue(R * 100) - form.slider_G.setValue(G * 100) - form.slider_B.setValue(B * 100) + form.slider_R.setValue(round(R * 100)) + form.slider_G.setValue(round(G * 100)) + form.slider_B.setValue(round(B * 100)) form.frame_color.setStyleSheet( "background-color: rgb(%d,%d,%d)" % ( R * 0xFF, G * 0xFF, B * 0xFF)) From c33c916e7c21a86e220260d4ed08d9c26f7c6da4 Mon Sep 17 00:00:00 2001 From: Jarrett Johnson <36459667+JarrettSJohnson@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:08:57 -0400 Subject: [PATCH 3/6] APBS electrostatics plugin (#344) --- 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) From 246e0a4c2d2d05bf2736dbb57e1694617d96c13d Mon Sep 17 00:00:00 2001 From: Jarrett Johnson <36459667+JarrettSJohnson@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:55:18 -0400 Subject: [PATCH 4/6] Nucleic Acid builder and general improvements (#354) Fixes #102 #351 Migrating support from Incentive for the following: - Nucleic Acid builder in the Builder panel (Jarrett Johnson @JarrettSJohnson) - `fnab` command to build nucleic acid chains by sequence (Jarrett Johnson @JarrettSJohnson) - Improvements to nucleic acid building from native structures (Thomas Stewart @TstewDev) - highlight attachment points for improved usability (Thomas Holder @speleo3) Co-authored-by: Jarrett Johnson Co-authored-by: Thomas Stewart Co-authored-by: Thomas Holder --- data/chempy/fragments/atpA.pkl | Bin 0 -> 3448 bytes data/chempy/fragments/atpB.pkl | Bin 0 -> 3448 bytes data/chempy/fragments/atp_ttpA.pkl | Bin 0 -> 9118 bytes data/chempy/fragments/atp_ttpB.pkl | Bin 0 -> 9118 bytes data/chempy/fragments/ctpA.pkl | Bin 0 -> 3115 bytes data/chempy/fragments/ctpB.pkl | Bin 0 -> 3115 bytes data/chempy/fragments/ctp_gtpA.pkl | Bin 0 -> 9123 bytes data/chempy/fragments/ctp_gtpB.pkl | Bin 0 -> 9123 bytes data/chempy/fragments/gtpA.pkl | Bin 0 -> 3590 bytes data/chempy/fragments/gtpB.pkl | Bin 0 -> 3590 bytes data/chempy/fragments/gtp_ctpA.pkl | Bin 0 -> 9114 bytes data/chempy/fragments/gtp_ctpB.pkl | Bin 0 -> 9114 bytes data/chempy/fragments/ttpA.pkl | Bin 0 -> 3264 bytes data/chempy/fragments/ttpB.pkl | Bin 0 -> 3264 bytes data/chempy/fragments/ttp_atpA.pkl | Bin 0 -> 9121 bytes data/chempy/fragments/ttp_atpB.pkl | Bin 0 -> 9121 bytes data/chempy/fragments/utpA.pkl | Bin 0 -> 2433 bytes modules/pmg_qt/builder.py | 296 ++++++++-- modules/pmg_tk/skins/normal/builder.py | 2 +- modules/pymol/api.py | 3 +- modules/pymol/editor.py | 722 +++++++++++++++++++++++++ modules/pymol/keywords.py | 1 + testing/tests/api/build.py | 1 - 23 files changed, 971 insertions(+), 54 deletions(-) create mode 100644 data/chempy/fragments/atpA.pkl create mode 100644 data/chempy/fragments/atpB.pkl create mode 100644 data/chempy/fragments/atp_ttpA.pkl create mode 100644 data/chempy/fragments/atp_ttpB.pkl create mode 100644 data/chempy/fragments/ctpA.pkl create mode 100644 data/chempy/fragments/ctpB.pkl create mode 100644 data/chempy/fragments/ctp_gtpA.pkl create mode 100644 data/chempy/fragments/ctp_gtpB.pkl create mode 100644 data/chempy/fragments/gtpA.pkl create mode 100644 data/chempy/fragments/gtpB.pkl create mode 100644 data/chempy/fragments/gtp_ctpA.pkl create mode 100644 data/chempy/fragments/gtp_ctpB.pkl create mode 100644 data/chempy/fragments/ttpA.pkl create mode 100644 data/chempy/fragments/ttpB.pkl create mode 100644 data/chempy/fragments/ttp_atpA.pkl create mode 100644 data/chempy/fragments/ttp_atpB.pkl create mode 100644 data/chempy/fragments/utpA.pkl diff --git a/data/chempy/fragments/atpA.pkl b/data/chempy/fragments/atpA.pkl new file mode 100644 index 0000000000000000000000000000000000000000..6ef22ce9d2f0260d718d9c23ab31d28ca40fb56e GIT binary patch literal 3448 zcma)DA;YwQu#fa3VNd~5vrC*vrD_$%v zARvnipdwJj4crkC+!7TK0TGYKN2SLHI36FA;{za`9>04t(`8{ih128lhc!l18o7QBqr5N zGU7g!wJ?gpXi>$9UxyRVJ9$V#$SsXMJg0{h$~~Kf94sQLUzgY9*FtlBt=-Qn;R*ZV=sdef8@lq}|e}RTsUiNZLAo!Q+6b zrU}NW4t01(COWb-$Bh)mb4L|+Q>H~rW7dxAr4(!z|jJ z=sPsSi-qoJM8}l0`Y@v-S946IFpWE=qocB7Pm)DTWm|m`E0VS?OPpBf4p(gz%ow(h z9(=lT(P7aXw^NwO9e1E(^o~tDS{kwUR1%}{IUee0M8~YO`mmrQPjlQ!p^iHwbX@1! z*3F`&;)_9EB=h!P>&21&K5D9Qu3%|}sraHE8A?z+FTNA|EHaqieoRDI>h28x!4$3x9O>VxcPeT~-ABAE zS;`&vtCJOpOMmT3VxcQslVMR>T@<3CNK-sO!NU~~qQV|n%#)?8%V&Er85KkCh-(Tg zPD@dQ3Y(^Qh{6)CXhwx?;pHStuKTBbUrOQU?OVk$hGmcoV#yQXNN;N^-|RG3dC zySd`R{GaiNU7QzEA3^PQH%tXzTKB?^iegRSrx4%@1r^rn@x3fr%GWQ{RgcY!BYGDN z(_m>@ieglhXo_VNRIX@a_oCI^!xi?^CwP(Q8~7sfhjuzFPwPUJpu(Xk9-$EAiV!Nw zQp*P36m79e{JVG*bM<#I1Hx%_;Xp-c+?U#A_#)u|M4F*p9JI@sje`OJ-{C}M=%5f2 zRdX}MMPecu3g0u}Mh5p7D@Z1;Oe0delb8}!BCwK}aQ-GyO`YEAj|t8YLblYvrL&C$mhnysUmMzmXoH58uU=#wJJA~`ymWkjEn;b{tMIl7La{i)eT z^cfk}Q`o@KjUve*W<52>h(0UBCJLK5x`m;+I%+neTV;5T!t)$`K_s|{>u9bKeNl#O z6t;78he&dXMMo`0^d%Wyrtk_!cQQ0jNAry6t1|4Ou$!ZMM3Of!0l|p&$gr2fYaHDt z5`j4M)O;iQx(shnc$1@Vi6oy`b<}D^-#g`98HMCM(lcOu@Ozm@HK^RIQlJ{K(UUN7|~-g9H(%Cqu+_7n3U*k zIgIE@8NR1*ile9573jZPrAG9O3_nmf%h4Z2QZj&+rBKCFf{Tg%PftsGG}5j_V{J+Y MoNI=k;=W1$15Y8svH$=8 literal 0 HcmV?d00001 diff --git a/data/chempy/fragments/atpB.pkl b/data/chempy/fragments/atpB.pkl new file mode 100644 index 0000000000000000000000000000000000000000..75f5b424a20ac2f76b9f64e4dc282d7b8cf6cf96 GIT binary patch literal 3448 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 literal 0 HcmV?d00001 diff --git a/data/chempy/fragments/ctp_gtpA.pkl b/data/chempy/fragments/ctp_gtpA.pkl new file mode 100644 index 0000000000000000000000000000000000000000..dd25d6be2d0108d662600a2642fb427d0132eece GIT binary patch literal 9123 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 LukVAlr+MtZYqS*R literal 0 HcmV?d00001 diff --git a/data/chempy/fragments/ctp_gtpB.pkl b/data/chempy/fragments/ctp_gtpB.pkl new file mode 100644 index 0000000000000000000000000000000000000000..ba83fe533a8dc23a43cfe5d5dd2c0c21eb8aa699 GIT binary patch literal 9123 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 Tw*T*;{|a2S{u>-S>qh?{cuFmS literal 0 HcmV?d00001 diff --git a/data/chempy/fragments/gtpA.pkl b/data/chempy/fragments/gtpA.pkl new file mode 100644 index 0000000000000000000000000000000000000000..33ff99320ee646a5ca869a0182d696a8e83de1bc GIT binary patch literal 3590 zcma)vdu@0`2$ z-rV~Ap|;Mtu+l8xhd&f-kAyq?kx*Cwlj!%WN}#QCTp-k_w6+Vgf{n^jrBQ$se_frg zqop;{60EBeLcT?cKO#VCRRq#xNT-k?dNWKWQw!t4T;gc z1;|p_;}#hNZ#vQqkx&4#Yr(45AWX**0dmI3kV_#iF7Ss!;YP64LcZvcvR0a%_@qa2 zT2IGV5VNhD)`fw>GX@(13KV!#aG(YXy=MGw?+o}ttzcIRIT2RcgHXf?4>-KpNZ?RQ zU9cnIQ^GKaqNVcbZ#xB2|erL;_HZ1nax0wV}BkhRp6VnVjBK9B65T z5@J%#B&EZrvKEF?7$&M%_;4)Ta$>K9kVhK1Z)P_OVnt0&^%y8>0XJ5c)xZeV&MT`1 zr&^>sRIEnDDz41^s|!gL)oNU*mSAWmshVjdg&Vl(M$zNCK7Fl(v_~4c{9-)|Vz#Ac z@w?YlUIU|4yE?EV6&-1s<0cBDxuX=jacHTf5vxb_aEEoz*uD;TWhsbSH*Ofyx0`Df z6%wNTyGcV+x@NkW!Y$l%E1L4Ru5lub-5l`pAeLvH6~mFlyrF4qQr)DZBSUkHqcEO3 zZbQeA->24NMvqjoVIF?tj0rZMgn{mGPbh^lbPU`_20F}|qnttocT7M>@#Wo^5FN$s z`&epE^!nZ@^%&@mYIIa4)rT1!nVMrFg-P5o8671L> z!ju8~=*HERiHn+;ImwhHYEVQ%i$(oQh#;T*v(#)##X(R3BOBuxO4uDY&>p zLdS6TmM)fBD!ACngP3L4wH_Qv^if_4(+BLsiG5hmA!v>n6x`f#7n`dUS6N1nWXDey zM6YL+IWb5$%1U8oQVszf*_z{S3Lfsb2OajqZQU%jt4I7N#)9av^ucU|IoOd=RR*)P zO;QE3`)-me@q?%TCdo#TRTB{k_j1u36ct~nmoS~jb?KbIgXozfi(@#Vi`?Z-6F&7oXVym2((BnET@blK=I?=mFcrWE3qhjDLa!`@0Dej|iKUX}!UP;kP zG~g>q*?y#p1<{M~1+8~cRRQyoQskl{Pg6Wd!OIm7p~A8AQa4v*I^xGl^m2Uf>k9V- zn4i={<)Ol+DITV4C&vr^jm@lM{w<@j<=C@NuLQVJU?@-;;rg?g@NK!t5& zeCxUX9=^YxEB;tmDPfRMOaxz2ihNWQXbL}tMy^m$kr_F}GrKN~{<8}+d!pwzt1QqJ z)f1r!6$Af<6riF|Q#4ahxuOLXR_D=fmRZWy7wy&l6IC?{7NKI`6;+4|yQX-ALMvAU z*t>{N3kD|-)-zZ1x^GSo26`9n$q-CxqU@+B(i9;IZCnAUaKuk0d@0&_82xwt(C$8p zswogos*562I68dsi;OQ6Y=rh&h=_eIQZsN+fDV3%S}em73QI-RTnn8dF_9F7UrX?= zgx8p5Boz<8!SM@FLze!Z%&p#>yrLclO{}_8dRZ*AWrGi~73}}}OD=9qA z(Nzph*U?lX`h*NmQdrH=H6lqP89JI~M4ysjEroR)eVU<(tm#Jd85!17*uc?^BFP|T zJ!^&$-6X?i3eR%%IfiEHsM&}!Yv zu^C991xEC!3|~_?#?j*<$tMMR0fk2NgbXJsoZ{#=tbjtj0J{-AEyK4IzT@Z_krWcU zp0&t`elNoh6n^CBPizJfFVLM%wS_}%N;uM@ Nw8O8p@LPv(-2W1`=#2mX literal 0 HcmV?d00001 diff --git a/data/chempy/fragments/gtpB.pkl b/data/chempy/fragments/gtpB.pkl new file mode 100644 index 0000000000000000000000000000000000000000..9458716ae2c6e44512faae28724b69edd88a3686 GIT binary patch literal 3590 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 literal 0 HcmV?d00001 diff --git a/data/chempy/fragments/gtp_ctpB.pkl b/data/chempy/fragments/gtp_ctpB.pkl new file mode 100644 index 0000000000000000000000000000000000000000..7d23aceaf102b20bd55a8a773cb54a32706060fa GIT binary patch literal 9114 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 xl}0ot!l literal 0 HcmV?d00001 diff --git a/data/chempy/fragments/ttpB.pkl b/data/chempy/fragments/ttpB.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2df8e6660c03a166dd3c72d2ed2877fa2f2de1a5 GIT binary patch literal 3264 zcma)HcWc={&7b7t&Uepu&fUGc zH<3uPZy=PCmvZh5SI)ulOt&k%pGYcmB2#4$_K3`lc|^TVCx7v@JBoulOvg!40dHGG1smQa^O(t z^fC#!patokbTSU*T~MLdz_sBB2bEJKs3K6U3L?p5DhkdnsNwy&Z@<_p;7q?zpWL3O zf%or?N)`i|WlZ$MWvC67;6Nw1f;Rl_ABcyOF>os`Mx^Ba1k^EN0gMWkBY{J`p+qJg zmQyfVED4t4*z)KKxWMoI>*&!u{$hy~Io*?v!-YuDzL&&$miEJ#xmlCR6Ex#MZxqIg zCdDR}WWow@cIFRC8QHU@~(w@P5HGm3E8{kLD0c)Dhj`Yi$5t>&6FDhIjL4bF+YR&+n!b zO%}~`DS@fXbQ$mW{;byS7bYG$Gf0)*U-!=|V4#}H(FmBGlZW?&cp1)$gmIi1<$H;wH(P7gZO$3^mqXql;_w!u^npzk=Yug|Vyk~bD z$zw3o2Rd4d>cfT(yXKfq;40?08vC$s-{YaFh4HJ73pB{(+t$-ScleqcV8)1j4C38o zM~6dmTtnbm=C}?WPW7(!3uEX1mS>K_rZf$7M*tny7uAOY9c7y11_EB@5OA%uE$tV^ zL>GCOy126Un53)0ux z4oix1)S|GjuVg!?L0jE>yTPVf73UP9~yo zXBWiyVP}w;ILJYqoj(#1BnkBKimeNP7foV`%--mD%)k>sN;KnhqfR|Lq~(;HgnrS4 z^_zG_ou8HL!akGPAu26mK)JQ33d>8fuS$*V!2qMqZLNNcL?lZ=)1gF zC06UG(}?bs;5`ES82UahR*O!(fEpvZUxEV!4l?uuiq_~^YmMk32|gt75kqslSR>Zz xsLP1vCHR=YCk*|RqHM9~3b<8N4!Usv>wg|XeW_%hoJ#k~{cyMoKFfrs{RhXAlBxgz literal 0 HcmV?d00001 diff --git a/data/chempy/fragments/ttp_atpA.pkl b/data/chempy/fragments/ttp_atpA.pkl new file mode 100644 index 0000000000000000000000000000000000000000..d51e3705057b146ee032c6fdf99c0a1fdf0bc3d8 GIT binary patch literal 9121 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
64RgxP#)sBmqrk ziY8~7CTHvH95Sf^lhn}t>v84I_#;RrJxe<=;3idIa;}(@^Lm>|bCLiiiD+`ZXmWvR zQmeCBWKsvKV?0(puAJuc>|}BvbR%#RcX=tR7jx3k+oVrU?2wa0FtMRYqiE7(nl$Uo zOD40yn5d(f zIvqOeB%LnMIaspa50iRa2?v+dV-UEa{Vwd`I_`2dU(Cxzy+x!elnf#RP-KB9vd|P+ zq_c}j+!zZrV$Gi%2)^01!z*k;_C8zbSIL&aNPl zE8z=hn~?}t)Y?hiXcP!sLeE<+QaPGk1tONExw?;tY-v(JWFU%MBZ^#Wid?6&>q+DW z5IHi|>Bsq9arL1+8~AX1jU9Tph5hVy#@yS4`X7Llqk>^5;}mi88rE=?+kq@l>|qR1Vl$elX7i$v~*jQsiQ zS{KgmN;URKVArx`-4+q|Shh^e$UVJ9q!~#AkwGZ3Tok$26j`CO`$*(|$VmMD1wAB^ z?!Lp1L16FI1wjaKX|S+!kDbc8MUw~mWikj%($VBW(c~f1wI|3c z*}%Tlne|o~sAOLfWj6L#W-!QPq0Gyo%qyl$KxaXcc@-b8Bctpj!;9G<6tmc4?$}cH znsuX8u-7Fr(%UBsWQL&3CQ;@MQ)aWy-XxhV@TJ2m*&wfE1HY}Sw#r~7`<9rQtr8jO zG7SNlY?OIhl-Xv=yrZ*sN#;Gs3>332$b%=yi`jsrn0;T&%m)$~X=buPCI@Al3$3K`BEa&Co@>p=7LNf%JhgbdrX+D(D zWtG9I_8T!X-%4bpnaKm0e3aQI%6wygO&v2F@Uc<`x{^bix)L{gA0SeYg4y(L%;Vv?lIax9LMt4eub8bJ$(hG87u<9VJMgekSI6^hY1U&!;!9KKudy|7(nGU;}6vF z1Y^12mZcpotKe8|FpeV#W?@f11&4uPF$xX=NEFP*VZwqraHMOw(2`&t2A;qVWyAd_ z7&yG*848@6HnzB+hcB^voR$x#ih>2$(@()-5FCzzLje*6?Kn(Wun>-Ptq58Y9EO1> z7_qO_4mzH|_xl&XCoJ&}_wDr%aKXxPS}~j|?!n>M(^pWs2Zw`T2?~|~Bnp<|FkwLl z9O>E!Xi4xG3_O9~AKK|huVBh6^Sd!{?p*m}Jp^IF6Sa{zj&PG4i#`1mECIn%6g&`sUK_ZV zRspT9O`ucbYBe_oN`w;WrFZP`Uf-)}6QL7cGZ!ewhThkAdNr?4n}od)@YXxRF*iKu z>CjoNd7#y`GoS_WT5U20Fldx_E)5k4Lq*P&@>7Uch(D9_Qw=`G!bi#ZO5&@CpT_y= z#78SuK3dLK6F-CanVdh%;A1WG$H@7!i9d&Ue5p^@e=hN{N}Pp{mGkEje?IXSaK6^y z<1KugoS#K}9r5*?Zy+At=~?)AIp0Wp6Ya=wfBBqiCxC(HTy#9u`G z0?scq_;d?DK+Z2B{$k=U;k-_KvNFKn@m5Tc^Oq8T8S#G3UvBUzmiY(D`74OOlK87Q ze>L$b%0LUBD(9~u{#xR%xs`*@+^G5oZmqFOT=&F{L2QPZ{Z8%{42xj4N5mH@MHc^JIlq(mkBR?;^Sg*IQifUPA1>!VC4M*Y zpK<}M0}~Re@BLSZ%sOs>iqv= TEdTAH{Q_LA_A9hqjide#H+n2o literal 0 HcmV?d00001 diff --git a/data/chempy/fragments/utpA.pkl b/data/chempy/fragments/utpA.pkl new file mode 100644 index 0000000000000000000000000000000000000000..11ae0560e50b7921a0c6afcac71bf8cfb2dbbc72 GIT binary patch literal 2433 zcmZA1No*WN6b4|=zIs+Kcu6u2At^91cnbswF2@OtDdGj_U_wmB_Oxk($CKwJwh0v` zEHQ~$gpjy!z=#tZ5JFKT5Yo&Mp@fR~?kmjZj9G{<_rf?a@x~~JYGUFYF>#06FeA*h6~#FuWRw(<*BPZa zq-9h?n1}hCQ7wl$8P$tu+pL>7G{~rtFdy?fXEkwXmeCFo1)R}N4!dNOA!PVF{LWf`>Wu$>@lPQqEaNIXoaEjj)u}A+}+dp#j}HHeblYgQL*@{|W2G zp)fpD0LI~AJcP5x+zcGkUApd`%E170nIY!VBRD*DYGxrnj)!d!2mEu(`SHRu4C$^R z-8(auH(*$lo-?^cctq>jxO%l*RaK??CJJ~$;n7iejA%4?-k-ryjb@@U*2;-Xhl)62 zh$D(k=p2p{Q#{@rU~YW0+r+1t_>@6&SoaLJ51EMnHTEmw}N ziK1LPIhn;msUsYouvHJLp6zN!rFw#BJ)6B3slaIS*`EZ4y)J!&S6h9O!%189q3Ts? zpXjx(d!Oj_MrS9*Nut+L?6qzSqq{EbS;uj$9Q|}ZUTyU$4ySC@i>gnl)(%*zV+XRR zCf`}f2#hv=HX@61JlbEz5w!zj9B_lT_SKK7U#V8FU6s8~pxU_JTSP;nO>Z2>EA|>r zj8iO>n&V*DY5>)MQmtMs)zRxDstwmZLITx>`7g4fC~aOoC<>)M&B3(QAgV#7TD^8w z_Bw@XQ{#JOfzi6R%%UjDi8oJXuu$qV9GfB&npRBN}Z zc@8tS8b&p&RIAsH%3ceo?mY7Dn!sr4M|n)Oa_GYw>o}r%JgaVA z)t28@sZ`S+e2Z7yf#7Q28Wu{O<1lZlQB~%rx>$$(z1xC}GR#Dmvwr3pb z84ioKT8(P8QU~xYa1XT07uNb3h?igq(K>6n8cl3%Yw4d8&1(*HnZt878bdUuP|>Qp zucPA4I*Vxi;@9g)Xf)9Oc~%sqYu4vLt^aur+(zSw#uX}d)jcRt$EKbW%m4nEg?hf* zEsN4G-91Ir75V~)qKzgHO(@h_erLt<&m*dScIwhT&Y-20evio)OrbAwc*#bSh$asP2KN*m-)F{02_4zJm04Wc#M b(E}Ce>xd@RkxFfSU`zi_%l83M{A>FU6T!W- literal 0 HcmV?d00001 diff --git a/modules/pmg_qt/builder.py b/modules/pmg_qt/builder.py index 15fc3ba5a..7c69f47a0 100644 --- a/modules/pmg_qt/builder.py +++ b/modules/pmg_qt/builder.py @@ -13,7 +13,6 @@ Qt = QtCore.Qt from pymol.wizard import Wizard -from pymol.parsing import QuietException from pymol import editor from pymol import computing @@ -306,7 +305,7 @@ def __init__(self, _self=pymol.cmd): RepeatableActionWizard.__init__(self,_self) self.mode = 0 - def do_pick(self, bondFlag): + def do_pick(self, bondFlag,*,_self=pymol.cmd): if self.mode == 0: self.cmd.select(active_sele, "bymol pk1") editor.attach_fragment("pk1", self.fragment, self.position, self.geometry, _self=self.cmd) @@ -328,14 +327,9 @@ def toggle(self, fragment, position, geometry, text): self.setActionHash( (fragment, position, geometry, text) ) self.activateRepeatOrDismiss() - def create_new(self): - names = self.cmd.get_names("objects") - num = 1 - while 1: - name = "obj%02d"%num - if name not in names: - break - num = num + 1 + def create_new(self,*,_self=pymol.cmd): + self.cmd.unpick() + name = self.cmd.get_unused_name("obj") self.cmd.fragment(self.fragment, name) if not self.getRepeating(): self.actionWizardDone() @@ -356,7 +350,7 @@ def combine(self): def get_panel(self): if self.getRepeating(): return [ - [ 1, 'Attaching Multiple Fragmnts',''], + [ 1, 'Attaching Multiple Fragments',''], [ 2, 'Create As New Object','cmd.get_wizard().create_new()'], [ 2, 'Combine w/ Existing Object','cmd.get_wizard().combine()'], [ 2, 'Done','cmd.set_wizard()'], @@ -371,34 +365,56 @@ def get_panel(self): ] -class AminoAcidWizard(RepeatableActionWizard): +class BioPolymerWizard(RepeatableActionWizard): - def __init__(self, _self=pymol.cmd, ss=-1): + HIGHLIGHT_SELE = "" + + def __init__(self, _self=pymol.cmd): RepeatableActionWizard.__init__(self,_self) self.mode = 0 - self.setSecondaryStructure(ss) + self._highlighting_enabled = False - def setSecondaryStructure(self, ss): - self._secondary_structure = ss + def __enter__(self): + '''Context manager for temporarily disabling attachment point highlights + ''' + self.highlight_attachment_points(False) + + def __exit__(self, *_): + self.highlight_attachment_points() + + def cleanup(self): + self.highlight_attachment_points(False) + RepeatableActionWizard.cleanup(self) + + def highlight_attachment_points(self, show=True): + '''Show spheres for potential attachment points + + :type show: bool + :param show: Switch to show or hide the highlights + ''' + if self._highlighting_enabled: + fn = self.cmd.show if show else self.cmd.hide + fn('spheres', self.HIGHLIGHT_SELE) def attach_monomer(self, objectname=""): - editor.attach_amino_acid("?pk1", self.aminoAcid, object=objectname, - ss=self._secondary_structure, - _self=self.cmd) + raise NotImplementedError + + def combine_monomer(self): + raise NotImplementedError def do_pick(self, bondFlag): # since this function can change any position of atoms in a related # molecule, bymol is used if self.mode == 0: - self.cmd.select(active_sele, "bymol pk1") + self.cmd.select(active_sele, "bymol ?pk1") try: with undocontext(self.cmd, "bymol ?pk1"): - self.attach_monomer(self.aminoAcid) - except QuietException: - fin = -1 + self.attach_monomer() + except pymol.CmdException as exc: + print(exc) elif self.mode == 1: - self.cmd.select(active_sele, "bymol pk1") - editor.combine_fragment("pk1", self.aminoAcid, 0, 1, _self=self.cmd) + self.cmd.select(active_sele, "bymol ?pk1") + editor.combine_monomer() self.mode = 0 self.cmd.refresh_wizard() @@ -406,23 +422,25 @@ def do_pick(self, bondFlag): if not self.getRepeating(): self.actionWizardDone() - def toggle(self, amino_acid): - self.aminoAcid = amino_acid - self.setActionHash( (amino_acid,) ) - self.activateRepeatOrDismiss() + def toggle(self, monomer): + self._monomer = monomer + self.setActionHash( (monomer,) ) + if self.activateRepeatOrDismiss(): + # enable temporary sphere highlighting of attachment points, but only + # if currently no spheres are displayed + self._highlighting_enabled = self.HIGHLIGHT_SELE and self.cmd.count_atoms( + '(rep spheres) & ({})'.format(self.HIGHLIGHT_SELE)) == 0 - def create_new(self): - names = self.cmd.get_names("objects") - num = 1 - while 1: - name = "obj%02d"%num - if name not in names: - break - num = num + 1 - self.attach_monomer(self.aminoAcid) + self.highlight_attachment_points() + def create_new(self, *, _self=pymol.cmd): + self.cmd.unpick() + name = self.cmd.get_unused_name("obj") + self.attach_monomer(name) if not self.getRepeating(): self.actionWizardDone() + else: + self.cmd.unpick() def combine(self): self.mode = 1 @@ -431,29 +449,67 @@ def combine(self): def get_prompt(self): if self.mode == 0: if self.getRepeating(): - return ["Pick locations to attach %s..."%self.aminoAcid] + return ["Pick locations to attach %s..." % self._monomer] else: - return ["Pick location to attach %s..."%self.aminoAcid] + return ["Pick location to attach %s..." % self._monomer] else: - return ["Pick object to combine %s into..."%self.aminoAcid] + return ["Pick object to combine %s into..." % self._monomer] def get_panel(self): if self.getRepeating(): return [ [ 1, 'Attaching Multiple Residues',''], [ 2, 'Create As New Object','cmd.get_wizard().create_new()'], - [ 2, 'Combine w/ Existing Object','cmd.get_wizard().combine()'], [ 2, 'Done','cmd.set_wizard()'], ] else: return [ [ 1, 'Attaching Amino Acid',''], [ 2, 'Create As New Object','cmd.get_wizard().create_new()'], - [ 2, 'Combine w/ Existing Object','cmd.get_wizard().combine()'], [ 2, 'Attach Multiple...','cmd.get_wizard().repeat()'], [ 2, 'Done','cmd.set_wizard()'], ] +class AminoAcidWizard(BioPolymerWizard): + + HIGHLIGHT_SELE = "(name N &! neighbor name C) | (name C &! neighbor name N)" + + def __init__(self, _self=pymol.cmd, ss=-1): + BioPolymerWizard.__init__(self,_self) + self._monomerType = "Amino Acid" + self.setSecondaryStructure(ss) + + def setSecondaryStructure(self, ss): + self._secondary_structure = ss + + def attach_monomer(self, objectname=""): + with self: + editor.attach_amino_acid("?pk1", self._monomer, object=objectname, + ss=self._secondary_structure, + _self=self.cmd) + + def combine_monomer(self): + editor.combine_fragment("pk1", self._monomer, 0, 1, _self=self.cmd) + +class NucleicAcidWizard(BioPolymerWizard): + + HIGHLIGHT_SELE = "(name O3' &! neighbor name P) | (name P &! neighbor name O3') | (name O5' &! neighbor name P) " + + def _init(self, form, dbl_helix, nuc_type): + self._monomerType = "Nucleic Acid" + self._form = form + self._dbl_helix = dbl_helix + self._nuc_type = nuc_type + return self + + def attach_monomer(self, objectname=""): + with self: + editor.attach_nuc_acid("?pk1", self._monomer, object=objectname, + nuc_type=self._nuc_type, form=self._form, + dbl_helix=self._dbl_helix, _self=self.cmd) + + def combine_monomer(self): + editor.combine_nucleotide("pk1", self._monomer + self._form, 0, 1, _self=self.cmd) class ValenceWizard(RepeatableActionWizard): @@ -950,6 +1006,14 @@ def collectPicked(self_cmd): return result +############################################################# +### Nucleic Acid helper class + +class NucleicAcidProperties: + def __init__(self, form="B", double_helix=True): + self.dna_form = form + self.dna_dbl_helix = double_helix + ############################################################# ### Actual GUI @@ -995,8 +1059,15 @@ def __init__(self, parent=None, app=None): self.protein_tab = QtWidgets.QWidget() self.protein_tab.setLayout(self.protein_layout) + self.nucleic_acid_layout = QtWidgets.QVBoxLayout() + self.nucleic_acid_layout.setContentsMargins(5, 5, 5, 5) + self.nucleic_acid_layout.setSpacing(5) + self.nucleic_acid_tab = QtWidgets.QWidget() + self.nucleic_acid_tab.setLayout(self.nucleic_acid_layout) + self.tabs.addTab(self.fragments_tab, "Chemical") self.tabs.addTab(self.protein_tab, "Protein") + self.tabs.addTab(self.nucleic_acid_tab, "Nucleic Acid") self.getIcons() @@ -1082,6 +1153,76 @@ def __init__(self, parent=None, app=None): self.protein_layout.addWidget(self.ss_cbox, 2, lab_cols, 1, 4) self.ss_cbox.currentIndexChanged[int].connect(self.ssIndexChanged) + self.nucleic_acid_dna_layout = QtWidgets.QGridLayout() + self.nucleic_acid_dna_layout.setContentsMargins(5, 5, 5, 5) + self.nucleic_acid_dna_layout.setSpacing(5) + self.nucleic_acid_rna_layout = QtWidgets.QGridLayout() + self.nucleic_acid_rna_layout.setContentsMargins(5, 5, 5, 5) + self.nucleic_acid_rna_layout.setSpacing(5) + + + self.nucleic_acid_tab = QtWidgets.QTabWidget() + self.nucleic_acid_layout.addWidget(self.nucleic_acid_tab) + self.dna_tab = QtWidgets.QWidget() + self.dna_tab.setLayout(self.nucleic_acid_dna_layout) + self.rna_tab = QtWidgets.QWidget() + self.rna_tab.setLayout(self.nucleic_acid_rna_layout) + + self.nucleic_acid_tab.addTab(self.dna_tab, "DNA") + self.nucleic_acid_tab.addTab(self.rna_tab, "RNA") + + self._nuc_acid_prop = NucleicAcidProperties() + + dna_buttons = ( + ("A", "Deoxyadenosine", None, lambda: self.attach_nuc_acid("atp", "DNA")), + ("C", "Deoxycytidine", None, lambda: self.attach_nuc_acid("ctp", "DNA")), + ("T", "Deoxythymidine", None, lambda: self.attach_nuc_acid("ttp", "DNA")), + ("G", "Deoxyguanosine", None, lambda: self.attach_nuc_acid("gtp", "DNA")), + ("@Form:", None, None, None), + ("#A", None, False, lambda: setattr(self._nuc_acid_prop, 'dna_form', "A")), + ("#B", None, True, lambda: setattr(self._nuc_acid_prop, 'dna_form', "B")), + ("@Helix:", None, None, None), + ("#Single", None, False, lambda: setattr(self._nuc_acid_prop, 'dna_dbl_helix', False)), + ("#Double", None, True, lambda: setattr(self._nuc_acid_prop, 'dna_dbl_helix', True)) + ) + for col_num, btn_pkg in enumerate(dna_buttons): + btn_label, btn_tooltip, default_activated, btn_command = btn_pkg + if btn_label[0] == '@': + btn = QtWidgets.QLabel(btn_label[1:]) + radio_group = QtWidgets.QButtonGroup(self) + elif btn_label[0] == '#': + btn = QtWidgets.QRadioButton(btn_label[1:]) + btn.toggled.connect(btn_command) + btn.setChecked(default_activated) + radio_group.addButton(btn) + else: + btn = makeFragmentButton() + btn.setText(btn_label) + btn.setToolTip(btn_tooltip) + btn.clicked.connect(btn_command) + self.nucleic_acid_dna_layout.addWidget(btn, 0, col_num) + + + rna_buttons = ( + ("A", "Adenosine", "atp", lambda: self.attach_nuc_acid("atp", "RNA")), + ("C", "Cytosine", "ctp", lambda: self.attach_nuc_acid("ctp", "RNA")), + ("U", "Uracil", "utp", lambda: self.attach_nuc_acid("utp", "RNA")), + ("G", "Guanine", "gtp", lambda: self.attach_nuc_acid("gtp", "RNA"))) + + for col_num, btn_pkg in enumerate(rna_buttons): + btn_label, btn_tooltip, btn_filename, btn_command = btn_pkg + btn = makeFragmentButton() + btn.setText(btn_label) + btn.setToolTip(btn_tooltip) + btn.clicked.connect(btn_command) + self.nucleic_acid_rna_layout.addWidget(btn, 0, col_num) + + btn = QtWidgets.QLabel('Hint: Also check out ' + 'fiber and its ' + 'PyMOL wrapper') + btn.setOpenExternalLinks(True) + self.nucleic_acid_rna_layout.addWidget(btn, 0, col_num + 1) + buttons = [ [ ( "@Atoms:", None, None), @@ -1094,6 +1235,8 @@ def __init__(self, parent=None, app=None): ( " +1 ", "Positive Charge", lambda: self.setCharge(1,"+1")), ( " 0 ", "Neutral Charge", lambda: self.setCharge(0,"neutral")), ( " -1 ", "Negative Charge", lambda: self.setCharge(-1,"-1")), + ( "@ Residue:", None, None), + ("Remove", "Remove residue", lambda: self.removeResn()), ], [ ( "@Bonds:", None, None), @@ -1139,9 +1282,12 @@ def _(checked, n=setting): self.cmd.set(n, checked, quiet=0) else: btn.setChecked(not value) - @btn.toggled.connect def _(checked, n=setting): self.cmd.set(n, not checked, quiet=0) + if setting == 'suspend_undo': + self.setUndoEnabled(not value) + _ = self.setUndoEnabled + btn.toggled.connect(_) else: btn = makeFragmentButton() btn.setText(btn_label) @@ -1151,6 +1297,29 @@ def _(checked, n=setting): btn_row_layout.addWidget(btn) btn_row_layout.addStretch() + def setUndoEnabled(self, checked): + self.cmd.set('suspend_undo', not checked, quiet=0) + if not checked: + return + + on_per_object = set(oname for oname in self.cmd.get_object_list() + if self.cmd.get_setting_int('suspend_undo', oname)) + + n = len(on_per_object) + if n > 20: + on_per_object = sorted(on_per_object)[:15] + [ + "[{} more]".format(n - 15)] + + if on_per_object: + QMB = QtWidgets.QMessageBox + check = QMB.question(None, 'Enable for objects?', + 'Building "Undo" is disabled for the following objects:\n\n' + + '\n'.join(on_per_object) + '\n\n' + 'Enable "Undo" for these objects?', QMB.Yes | QMB.No) + if check == QMB.Yes: + for oname in on_per_object: + self.cmd.unset('suspend_undo', oname) + def getIcons(self): self.icons = {} # use old Tk icons @@ -1209,6 +1378,37 @@ def ssIndexChanged(self, index): if isinstance(w, AminoAcidWizard): w.setSecondaryStructure(index + 1) + def attach_nuc_acid(self, nuc_acid, nuc_type): + self._nuc_type = nuc_type + picked = collectPicked(self.cmd) + if len(picked)==1: + try: + with undocontext(self.cmd, "byobject %s" % picked[0]): + editor.attach_nuc_acid(picked[0], nuc_acid, + nuc_type=self._nuc_type, + object="", + form=self._nuc_acid_prop.dna_form, + dbl_helix=self._nuc_acid_prop.dna_dbl_helix, + _self=self.cmd) + except pymol.CmdException as exc: + print(exc) + except ValueError as exc: + print(exc) + self.doZoom() + else: + self.cmd.unpick() + NucleicAcidWizard(_self=self.cmd)._init(form=self._nuc_acid_prop.dna_form, + dbl_helix=self._nuc_acid_prop.dna_dbl_helix, + nuc_type=self._nuc_type).toggle(nuc_acid) + + def removeResn(self): + picked = collectPicked(self.cmd) + if picked == ["pk1"]: + self.cmd.select(newest_sele,"byres(pk1)") + self.cmd.remove(newest_sele) + else: + print("Select a single atom on the residue and press remove again") + def doAutoPick(self, old_atoms=None): self.cmd.unpick() if self.cmd.select(newest_sele,"(byobj "+active_sele+") and not "+active_sele)==0: @@ -1228,7 +1428,7 @@ def doAutoPick(self, old_atoms=None): def doZoom(self, *ignore): if "pk1" in self.cmd.get_names("selections"): - self.cmd.zoom("((neighbor pk1) extend 4)", 4.0, animate=-1) + self.cmd.center("%pk1 extend 9", animate=-1) def setCharge(self, charge, text): picked = collectPicked(self.cmd) @@ -1304,12 +1504,6 @@ def invert(self, _=None): self.cmd.unpick() InvertWizard(self.cmd).toggle() - def center(self): - if "pk1" in self.cmd.get_names("selections"): - self.cmd.zoom("pk1", 5.0, animate=-1) - else: - self.cmd.zoom("all", 3.0, animate=-1) - def removeAtom(self): picked = collectPicked(self.cmd) if len(picked): diff --git a/modules/pmg_tk/skins/normal/builder.py b/modules/pmg_tk/skins/normal/builder.py index 6cead85ad..478c2b8fe 100644 --- a/modules/pmg_tk/skins/normal/builder.py +++ b/modules/pmg_tk/skins/normal/builder.py @@ -345,7 +345,7 @@ def combine(self): def get_panel(self): if self.getRepeating(): return [ - [ 1, 'Attaching Multiple Fragmnts',''], + [ 1, 'Attaching Multiple Fragments',''], [ 2, 'Create As New Object','cmd.get_wizard().create_new()'], [ 2, 'Combine w/ Existing Object','cmd.get_wizard().combine()'], [ 2, 'Done','cmd.set_wizard()'], diff --git a/modules/pymol/api.py b/modules/pymol/api.py index 383d9e527..4b24bb8b6 100644 --- a/modules/pymol/api.py +++ b/modules/pymol/api.py @@ -259,7 +259,8 @@ vdw_fit from .editor import \ - fab + fab, \ + fnab from .computing import \ clean diff --git a/modules/pymol/editor.py b/modules/pymol/editor.py index eb16e301a..613ebeb58 100644 --- a/modules/pymol/editor.py +++ b/modules/pymol/editor.py @@ -1,3 +1,5 @@ +import math + import re import pymol cmd = __import__("sys").modules["pymol.cmd"] @@ -384,6 +386,659 @@ def _fab(input,name,mode,resi,chain,segi,state,dir,hydro,ss,quiet,_self=cmd): return r +_threeNA_to_OneNA = { "atp" : "A", "ctp" : "C", "gtp" : "G", "ttp" : "T", "utp" : "U"} +_oneNA_to_threeNA = { "A" : "atp", "C" : "ctp", "G" : "gtp", "T" : "ttp", "U" : "utp"} + +_base_pair = { "DNA" : {"atp" : "ttp", "ctp" : "gtp", "gtp" : "ctp", "ttp" : "atp" }, + "RNA" : {"atp" : "utp", "ctp" : "gtp", "gtp" : "ctp", "utp" : "atp" }} + +_oneNA_base_pair = {"DA" : "DT", "DC" : "DG", "DG" : "DC", "DT" : "DA", "A" : "U", + "C" : "G", "G" : "C", "U" : "A"} + +def iterate_to_list(selection: str, expression: str, *, _self=cmd): + outlist = [] + _self.iterate(selection,f"outlist.append(({expression}))", space={"outlist":outlist}) + return outlist + +def rename_three_to_one(nuc_acid, sele, nuc_type, *, _self=cmd): + """ + Renames nucleobase from 3-letter to 1-letter representation + + :param nuc_acid: (str) 3-letter nucleic acid representation + :param sele: (str) selection of nucleic acid to rename + :param nuc_type: (str) "DNA" or "RNA" + """ + new_name = _threeNA_to_OneNA[nuc_acid] + if nuc_type == "DNA": + new_name = "D" + new_name + _self.alter(sele, f"resn='{new_name}'") + +def fit_sugars(mobile, target, *, _self=cmd): + """ + Fits appending base pairs to form appropriate hydrogen bond + + :param mobile: (str) selection for the sense (main) strand + :param target: (str) selection for the antisense (opposing) strand + """ + try: + _self.pair_fit(f"{mobile} & name C1'", + f"{target} & name C1'", + f"{mobile} & name C2'", + f"{target} & name C2'", + f"{mobile} & name C3'", + f"{target} & name C3'", + f"{mobile} & name C4'", + f"{target} & name C4'", + f"{mobile} & name O4'", + f"{target} & name O4'", quiet=1) + except: + _self.delete(tmp_wild) + raise pymol.CmdException("Something went wrong when fitting the new residue.") + +def fit_DS_fragment(mobile_A, target_A, mobile_B, target_B, *, _self=cmd): + """ + Fits dummy fragment to the detected structure using atoms + on both stands for a more accurate alignment. + + :param mobile_A: (str) selection for the base being created and attached + :param target_A: (str) selection for the base selected to build on + :param mobile_B: (str) selection for the opposing base being created + :param target_B: (str) selection for the detected opposing base + """ + try: + _self.pair_fit(f"{mobile_A} & name C1'", + f"{target_A} & name C1'", + f"{mobile_A} & name C2'", + f"{target_A} & name C2'", + f"{mobile_A} & name C5'", + f"{target_A} & name C5'", + f"{mobile_A} & name O4'", + f"{target_A} & name O4'", + f"{mobile_A} & name O3'", + f"{target_A} & name O3'", + f"{mobile_A} & name P", + f"{target_A} & name P", + f"{mobile_B} & name O3'", + f"{target_B} & name O3'", + f"{mobile_B} & name C1'", + f"{target_B} & name C1'", + f"{mobile_B} & name C2'", + f"{target_B} & name C2'", + f"{mobile_B} & name C5'", + f"{target_B} & name C5'", + f"{mobile_B} & name P", + f"{target_B} & name P", + f"{mobile_B} & name O4'", + f"{target_B} & name O4'", quiet=1) + except: + _self.delete(tmp_wild) + raise pymol.CmdException("Something went wrong when fitting the new residue.") + +def add2pO(domain, nuc_acid, resv, *, _self=cmd): + if nuc_acid == "utp": #utp comes with O2' + return + c_2p = _prefix + "_c2p" + _self.select(c_2p, "%s & resi \\%i & name %s" % (domain, resv, "C2'")) + _self.unpick() + _self.edit(c_2p) + _self.attach("O", 4, 4) + _self.unpick() + _self.alter("(byres %s) & resi \\%i & name O01" % (domain, resv), "name=\"O2'\"") + +def move_atom_in_res(atom, dummy_res, new_res, twist, rise, *, _self=cmd): + prev_coords = _self.get_coords(f"{dummy_res} & name {atom}", state=1) + curr_coords = _self.get_coords(f"{new_res} & name {atom}", state=1) + + if curr_coords is None or prev_coords is None: + return + curr_coord = curr_coords[0] + prev_coord = prev_coords[0] + + r = math.sqrt(prev_coord[0] ** 2 + + prev_coord[1] ** 2) + old_phi = math.degrees(math.atan2(prev_coord[1],prev_coord[0])) + phi = old_phi - twist + phi = math.radians(phi) + new_pos = [r * math.cos(phi), + r * math.sin(phi), + prev_coord[2] - rise] + + trans = list(new_pos - curr_coord) + _self.translate(trans, f"{new_res} & name {atom}", camera=0) + +def move_new_res(frag_string, full_frag, old, old_oppo, double_stranded_bool=False, form="B", chain="A", antisense=False, *, _self=cmd): + """ + Attaches new residue (or pair) onto current nucleotide chain + + :param frag_string: (str) Name of appending nucleic acid or pair + :param full_frag: (str) Selection of the created fragment + :param old: (str) Selection of previous residue + :param old_oppo: (str) Selection of previous opposing residue + :param double_stranded_bool: (bool) Flag represing if double helix was detected + :param antisense (bool) Flag for antisense + :param form: (str) DNA form ('A'/'B') + """ + if form == 'B': + twist = -36.0 + rise = -3.375 + elif form == 'A': + twist = -32.7 + rise = -2.548 + else: + raise ValueError("Form not recognized") + + rise = rise if antisense else -rise + twist = twist if antisense else -twist + + dummy_fragment = _prefix + "_dummyfrag" + dummy_res_A = _prefix + "_dummyresA" + dummy_res_B = _prefix + "_dummyresB" + new_fragment = _prefix + "_newfrag" + new_res_A = _prefix + "_newresA" + new_res_B = _prefix + "_newresB" + + _self.select(new_fragment, f"{full_frag}") + _self.select(new_res_A,f"{full_frag} and chain A") + _self.select(new_res_B,f"{full_frag} and chain B") + + _self.fragment(frag_string, dummy_fragment, origin=0) + _self.select(dummy_res_A, f"{dummy_fragment} and chain A") + _self.select(dummy_res_B, f"{dummy_fragment} and chain B") + + if old_oppo == "none": + # This is the case where a single residue is being added + _self.select(dummy_res_A, f"{dummy_fragment}") + _self.select(new_res_A, f"{full_frag}") + + atoms_A = iterate_to_list(dummy_res_A, "name") + atoms_B = iterate_to_list(dummy_res_B, "name") + + #A new base is created by copying the coordinates of the previous + #base and doing a cylindrical rotation (phi degrees) and a translation + #down the z-axis by the rise amount + for atom in atoms_A: + move_atom_in_res(atom, dummy_res_A, new_res_A, twist, rise) + for atom in atoms_B: + move_atom_in_res(atom, dummy_res_B, new_res_B, twist, rise) + if double_stranded_bool == True: + fit_DS_fragment(dummy_res_A, old, dummy_res_B, old_oppo) + orient_flag = check_dummy_oriention(old,dummy_res_A) + if orient_flag == 0: + fit_sugars(dummy_res_A,old) + elif double_stranded_bool == False: + fit_sugars(dummy_res_A, old) + else: + raise pymol.CmdException("Double stranded bool was not provided to move_new_res") + + dummy_fragment_transform = _self.get_object_matrix(dummy_fragment) + _self.transform_object(full_frag, dummy_fragment_transform) + _self.delete(dummy_fragment) + +def check_dummy_oriention(old, dummy_res_A, *, _self=cmd): + dummy_orient = _prefix + "_dummy_orient" + orient_flag = 0 + orient_flag = _self.select(dummy_orient, f"({old} & name O4') within 1.0 of ({dummy_res_A} & name O4')") + + return orient_flag + +class NascentNucAcidInfo: + def __init__(self, fragment_name, nuc_acid, nuc_type, form, dbl_helix): + self.fragment_name = fragment_name + self.nuc_acid = nuc_acid + self.nuc_type = nuc_type + self.form = form + self.dbl_helix = dbl_helix + +def attach_O5_phosphate(_self=cmd): + if "pk1" not in _self.get_names("selections"): + raise pymol.CmdException("Selection must be pk1 to attach O5' phosphate") + + print("This building selection has an unphosphorylated O5' end.") + attach_fragment("pk1","phosphite",4,0) + # Initailize selection strings + P_center = _prefix + "_P_center" + H_extra = _prefix + "_H_extra" + O_one = _prefix + "_O_one" + O_two = _prefix + "_O_two" + O_three = _prefix + "_O_three" + + # Selection + _self.select(P_center,"n. P01") + _self.select(H_extra, f"h. and bound_to {P_center} or n. H02") + _self.select(O_one, f"n. O01 and bound_to {P_center}") + _self.select(O_two, f"n. O02 and bound_to {P_center}") + _self.select(O_three, f"n. O03 and bound_to {P_center}") + + # Removing unnecessary atoms + _self.remove(H_extra) + _self.remove(O_one) + + # Fix bonding + _self.unbond(P_center,O_three) + _self.bond(P_center,O_three,1) + _self.unbond(P_center,O_two) + _self.bond(P_center,O_two,2) + + # Rename P correctly + _self.alter(P_center,"name = 'P'") + + # Set Pk1 correctly + _self.select("pk1",P_center) + +def check_DNA_base_pair(sele_oppo_atom, selection, *, _self=cmd): + base_pair_dist = _prefix + "_base_pair_dist" + base_pair_bool = 0 + tmp_last_resn = iterate_to_list(selection,"resn") + tmp_last_resn_oppo = iterate_to_list(sele_oppo_atom,"resn") + + if len(tmp_last_resn_oppo) != 0: + last_resn = str(tmp_last_resn[0]) + last_resn_oppo = str(tmp_last_resn_oppo[0]) + + if (_oneNA_base_pair[last_resn] == last_resn_oppo and + _self.select(base_pair_dist, f"(byres {sele_oppo_atom}) within 3.5 of (byres {selection})") != 0): + base_pair_bool = 1 + else: + base_pair_bool = 0 + else: + print("check_DNA_base_pair has no opposing residue to check") + return base_pair_bool + +def get_chains_oppo (chain, tmp_connect, *, _self=cmd): + models = iterate_to_list(tmp_connect,"model") + + close_chains = [] + close_chains = _self.get_chains(f"({models[0]}) within 15.0 of {tmp_connect}") + close_chains = [c for c in close_chains if c != chain] + + return close_chains + +def get_new_chain (chain, tmp_connect, *, _self=cmd): + models = iterate_to_list(tmp_connect,"model") + model_chains = _self.get_chains(models[0]) + search_chain_flag = 0 + + if len(model_chains) != 0: + last_chain = f"{model_chains[-1]}" + last_chain_front = last_chain[:-1] + last_chain_back = last_chain[-1] + if last_chain_back != 'Z' and last_chain_back != 'z': + new_chain_back = chr(ord(last_chain_back)+1) + elif last_chain_back == 'Z': + new_chain_back = "ZA" + print("Z chain was detected. New chain will append A") + else: + new_chain_back = "za" + print("z chain was detected. New chain will append a") + else: + new_chain_back = "A" + + chain_oppo = last_chain_front + new_chain_back + return chain_oppo + +def check_valid_attachment(nascent, atom_selection_name, selection, resv, *, _self=cmd): + atom_selection_name_partner = "O3'" if atom_selection_name == "P" else "P" + atom_sele = _prefix + "atom_sele" + _self.select(atom_sele, f"{selection}") + bound = _prefix + "atom_sele_bound" + if _self.count_atoms(f"(bound_to {atom_sele}) & name {atom_selection_name_partner}") != 0: + _self.delete(tmp_wild) + raise pymol.CmdException(f"{atom_selection_name} already bonded!") + +def bond_single_stranded(tmp_editor, object, chain, resv, last_resi_sele, atom_selection_name, atom_name_oppo, *, _self=cmd): + """ + Forms a bond between the last atoms on selected structure and the newly created fragment + :param tmp_editor: (str) Object representing the newly created fragment + :param object: (str) object/model name of the selected structure + :param chain: (str) Chain ID + :param resv: (int) + :param last_resi_sele: (str) The selection string of the selected residue + :param atom_selection_name: (str) Name of the atom selected + :param atom_name_oppo: (str) Name of the corresponding opposing atom + """ + object_fuse = _prefix + f"_{chain}_fuse" + object_connect = _prefix + f"_{chain}_con" + + print("The program did not detect a double stranded structure, so the opposing residue will not be attached.") + + # Select and fuse + _self.select(object_fuse, f"{last_resi_sele} & name {atom_selection_name}") + _self.fuse(f"{tmp_editor} & chain {chain} & name {atom_name_oppo}", object_fuse, mode=3) + _self.select(object_connect, f"{last_resi_sele} & name {atom_selection_name} & chain {chain}") + + # Target is on the new fragment + object_bond_target = _prefix + f"_{chain}_con_target" + if (_self.select(object_bond_target, f"{object} & resi \\{resv} & name {atom_name_oppo} & chain {chain}") == 1): + bond_dist = _prefix + "_bond_dist" + if (_self.select(bond_dist, f"{object_connect} within 3.0 of {object_bond_target}") != 0): + _self.bond(object_connect, object_bond_target) + else: + print("Identified bond targets were too far apart, so this will not be bound") + else: + print("More than one bond target was identified, so this will not be bound") + +def bond_double_stranded(tmp_editor, object, chain, chain_oppo, resv, resv_oppo, last_resi_sele, prev_oppo_res, atom_selection_name, + atom_name_oppo, *, _self=cmd): + """ + Forms a bond between the last atoms on selected structure and the newly created fragment + :param tmp_editor: (str) Object representing the newly created fragment + :param object: (str) object/model name of the selected structure + :param chain: (str) Chain ID + :param chain_oppo: (str) Opposing chain ID + :param resv: (int) + :param resv_oppo: (int) + :param last_resi_sele: (str) The selection string of the selected residue + :param prev_oppo_res: (str) The selection string of the previous opposing residue + :param atom_selection_name: (str) Name of the atom selected + :param atom_name_oppo: (str) Name of the corresponding opposing atom + """ + object_fuse = _prefix + f"_{chain}_fuse" + object_oppo_fuse = _prefix + f"_oppo_{chain_oppo}_fuse" + object_connect = _prefix + f"_{chain}_con" + object_oppo_connect = _prefix + f"_oppo_{chain_oppo}_con" + + # Target is on the new fragment + object_bond_target = _prefix + f"_{chain}_con_target" + object_oppo_bond_target = _prefix + f"_oppo_{chain_oppo}_con_target" + + # Select and fuse + _self.select(object_fuse, f"{last_resi_sele} & name {atom_selection_name}") + _self.select(object_oppo_fuse, f"{prev_oppo_res} & name {atom_name_oppo}") + _self.fuse(f"{tmp_editor} & chain {chain} & name {atom_name_oppo}", object_fuse, mode=3) + + if ((_self.select(object_bond_target, f"{object} & resi \\{resv} & name {atom_name_oppo} & chain {chain}") == 1) and + (_self.select(object_connect, f"{last_resi_sele} & name {atom_selection_name} & chain {chain}") == 1)): + bond_dist = _prefix + "_bond_dist" + if (_self.select(bond_dist, f"{object_connect} within 3.0 of {object_bond_target}") != 0): + _self.bond(object_connect, object_bond_target) + else: + print("Identified bond targets were too far apart, so this will not be bound") + else: + print("More than one bond target was found on selected chain, so this will not be bound.") + + if ((_self.select(object_oppo_bond_target, f"{object} & resi \\{resv_oppo} & name {atom_selection_name} & chain {chain_oppo}") == 1) and + (_self.select(object_oppo_connect, f"{prev_oppo_res} & name {atom_name_oppo} & chain {chain_oppo}") == 1)): + bond_dist = _prefix + "_bond_dist" + if (_self.select(bond_dist, f"{object_oppo_connect} within 3.0 of {object_oppo_bond_target}") != 0): + _self.bond(object_oppo_connect, object_oppo_bond_target) + else: + print("Identified bond targets were too far apart, so this will not be bound") + else: + print("More than one bond target was found on opposing chain, so this will not be bound.") + +def attach_nuc_acid(selection, nuc_acid, nuc_type, object= "", form ="B", + dbl_helix=True, *, _self=cmd): + """ + Creates new nuc acid attached to existing PDB structure + :param selection: (str) selection of picked nascent chain (or nothing) + :param nuc_acid: (str) appending nucleic acid + :param nuc_type: (str) sugar type of nucleic acid + :param object: (str) name of appending nucleobase + :param form: (str) DNA structure form: A, B, or Z + :param dbl_helix: (bool) flag for double-strandedness + """ + original_sele = _prefix + "_original_sele" + _self.select(original_sele,selection) + + if nuc_type == "RNA" and form != 'A': + form = 'A' + dbl_helix = False + + nascent = NascentNucAcidInfo(nuc_acid + form, nuc_acid, nuc_type, form, dbl_helix) + nuc_acid_partner_temp = _base_pair[nuc_type][nuc_acid].lower() + nascent_partner = NascentNucAcidInfo(nuc_acid_partner_temp + form, nuc_acid_partner_temp, + nuc_type, form, dbl_helix) + + if _self.cmd.count_atoms(selection) == 0: + if object == "": + object = nuc_acid + + if dbl_helix: + frag_string = nascent.nuc_acid + "_"+ _base_pair["DNA"][nascent.nuc_acid] + nascent.form + _self.fragment(frag_string,object) + elif not dbl_helix: + _self.fragment(nascent.fragment_name, object, origin=0) + _self.alter(object, f"segi='A';chain='A';resv=1") + rename_three_to_one(nascent.nuc_acid, object, nascent.nuc_type) + + if nascent.nuc_type == "RNA": + add2pO(object, nascent.nuc_acid, 1) + + if _self.count_atoms(f"{object} & segi A & name P"): + _self.edit(f"{object} & segi A & name P") + elif _self.count_atoms(f"{object} & segi A & name O3'"): + _self.edit(f"{object} & segi A & name O3'") + _self.edit(f"{object} & segi A & name O3'") + _self.select("pk1", f"{object} & name O3' & chain A") + elif _self.select(tmp_connect, f'{selection}') == 1: + chain, name, object = iterate_to_list(selection,"chain,name,model")[0] + + if name == "O5'": + attach_O5_phosphate() + name = "P" + # FIXME Don't use `selection` as the name, or ensure that it's + # indeed just a name and not a selection expression. + _self.select(selection,f"name P & bound_to {original_sele}") + _self.delete("_pkdihe") + _self.select(tmp_connect,f"{selection}") + + if name == "P" or name == "O3'": + extend_nuc_acid(nascent, nascent_partner, selection, object, name, chain, _self=_self) + + _self.select("pk1","_tmp_editor_new_selection") + else: + _self.delete(tmp_wild) + raise pymol.CmdException("invalid connection point: must be one atom, name O3' or P") + + _self.show("cartoon", f"byobject {selection}") + _self.delete(tmp_wild) + +def extend_nuc_acid(nascent, nascent_partner, selection, + object, atom_selection_name, chain = 'A', *, _self=cmd): + """ + Creates new nuc acid (or pair) or attaches to PDB chain + :param nascent: (NascentNucAcidInfo) appending nucleic acid + :param nascent_partner: (NascentNucAcidInfo) partner of appending nucleic acid + :param selection: (str) selection string of selected residue + :param object: (str) object of selected residue + :param atom_selection_name: (str) O3' or P + :param chain: (str) chain ID + + FIXME Eliminate `tmp_connect` pre-condition (or at least document it + properly). Looks like `tmp_connect` must be the same as `selection`? + """ + # Making a temporary selection + original_sele = _prefix + "_original_sele" + _self.select(original_sele,selection) + + # Alter the segi to match the chain selection + chain_sele = "_chain_sele" + _self.select(chain_sele, f"chain {chain}") + _self.alter(chain_sele,f"segi = '{chain}'") + _self.delete(chain_sele) + + if not nascent.dbl_helix: + frag_string = nascent.fragment_name + _self.fragment(frag_string, tmp_editor, origin=0) + rename_three_to_one(nascent.nuc_acid, tmp_editor, nascent.nuc_type) + elif nascent.dbl_helix: + frag_string = nascent.nuc_acid + "_"+ _base_pair["DNA"][nascent.nuc_acid] + nascent.form + _self.fragment(frag_string, tmp_editor, origin=0) + else: + raise pymol.CmdException("No helix state selected") + + if _self.count_atoms(f"name {atom_selection_name}", domain=tmp_connect): + tmp_resv = iterate_to_list(tmp_connect,"resv") + + # Assign last res before adjustment is made + last_resv = int(tmp_resv[0]) + + if atom_selection_name == "O3'": + tmp_resv[0] = str(tmp_resv[0] + 1) + elif atom_selection_name == "P": + tmp_resv[0] = str(tmp_resv[0] - 1) + else: + raise pymol.CmdException("Something went wrong with resv loop in extend_nuc") + + # Resv and resi assignment and testing + resv = int(tmp_resv[0]) + resi = resv + + check_valid_attachment(nascent, atom_selection_name, selection, resv) + + reverse = False if atom_selection_name == "O3'" else True + + last_resi_sele = _prefix + "_last_resi" + _self.select(last_resi_sele, f"(byobject {selection}) & chain {chain} & resi \\{last_resv}") + + if not nascent.dbl_helix: + _self.alter(tmp_editor, f"chain='{chain}';segi='{chain}';resi=tmp_resv[0]", + space={'tmp_resv': tmp_resv}) + # Set parameters so that move_new_res can be used + prev_oppo_res = "none" + double_stranded_bool = False + + # Move and fuse + move_new_res(frag_string, tmp_editor, last_resi_sele, prev_oppo_res, double_stranded_bool, nascent.form, antisense=reverse) + _self.fuse(f"{tmp_editor} & name P", tmp_connect, mode=3) + + _self.select(tmp_domain, "byresi (pk1 | pk2)") + + if atom_selection_name == "O3'": + _self.bond(f"{tmp_domain} & resi \\{last_resv} & name O3'", + f"{tmp_domain} & resi \\{resv} & name P") + elif atom_selection_name == "P": + _self.bond(f"{tmp_domain} & resi \\{resv} & name O3'", + f"{tmp_domain} & resi \\{last_resv} & name P") + + elif nascent.dbl_helix: + #Initialize strings for selection + prev_oppo_res = _prefix + "_prev_oppo_res" + end_oppo_atom = _prefix + "_end_oppo_atom" + + #Initialize a double stranded bool for the detection of existing double strand + double_stranded_bool = False + base_pair_result = 0 + + # Get oppo information + atom_name_oppo = "O3'" if atom_selection_name == "P" else "P" + + # Get opposing chains and iterate looking for basepairs + chains_oppo = get_chains_oppo(chain, tmp_connect) + for tmp_chain_oppo in chains_oppo: + # Create selection strings + last_oppo_atom = _prefix + "_last_oppo_atom" + first_oppo_atom = _prefix + "_first_oppo_atom" + same_oppo_res = _prefix + "_same_oppo_atom" + + _self.select(last_oppo_atom, f"last (({object} and chain {tmp_chain_oppo} and (not {tmp_editor})) and polymer.nuc)") + _self.select(first_oppo_atom, f"first (({object} and chain {tmp_chain_oppo} and (not {tmp_editor})) and polymer.nuc)") + base_pair_last = check_DNA_base_pair(last_oppo_atom, original_sele) + base_pair_first = check_DNA_base_pair(first_oppo_atom, original_sele) + + same_base_flag = _self.select(same_oppo_res, f"({object} and (byres {last_oppo_atom}) and (byres {first_oppo_atom}))") + + if ((base_pair_first == 1 or base_pair_last == 1) and base_pair_result == 1 and same_base_flag == 0): + print("Multiple residues meet base pairing requirements. Building as if no opposing strand detected.") + base_pair_result = 0 + break + elif (base_pair_first == 1 and base_pair_last ==1): + chain_oppo = tmp_chain_oppo + base_pair_result = 1 + + check_DNA_base_pair(first_oppo_atom, original_sele) + atoms_first = iterate_to_list(_prefix + "_base_pair_dist","name") + check_DNA_base_pair(last_oppo_atom, original_sele) + atoms_last = iterate_to_list(_prefix + "_base_pair_dist", "name") + + if (len(atoms_first) > len(atoms_last)): + _self.select(end_oppo_atom,first_oppo_atom) + else: + _self.select(end_oppo_atom,last_oppo_atom) + + elif (base_pair_last == 1): + base_pair_result = 1 + chain_oppo = tmp_chain_oppo + _self.select(end_oppo_atom,last_oppo_atom) + elif (base_pair_first == 1): + base_pair_result = 1 + chain_oppo = tmp_chain_oppo + _self.select(end_oppo_atom,first_oppo_atom) + else: + print("No based pair was found on chain ", tmp_chain_oppo) + + if (base_pair_result == 1): + double_stranded_bool = True + # Confirm base pair result and set _base_pair_dist + check_DNA_base_pair(end_oppo_atom, original_sele) + + tmp_last_resv_oppo = iterate_to_list(end_oppo_atom,"resv") + if len(tmp_last_resv_oppo) == 1: + last_resv_oppo = tmp_last_resv_oppo[0] + else: + # This is arbitrary and may be changed. Using neg selected resv for now. + last_resv_oppo = -last_resv + + _self.select(prev_oppo_res, "byres (_tmp_editor_base_pair_dist)") + elif (base_pair_result == 0): + last_resv_oppo = -last_resv + chain_oppo = get_new_chain(chain, tmp_connect) + else: + raise pymol.CmdException("Base pairing result is not returning 0 or 1") + + # Alter the opposing segi to match chain + chain_oppo_sele = "_chain_oppo_sele" + _self.select(chain_oppo_sele, f"chain {chain_oppo}") + _self.alter(chain_oppo_sele,f"segi = '{chain_oppo}'") + _self.delete(chain_oppo_sele) + + # Get oppo resv value based on selection + if atom_selection_name == "O3'": + resv_oppo = last_resv_oppo - 1 + elif atom_selection_name == "P": + resv_oppo = last_resv_oppo + 1 + + # If picking O3', check O5' phosphate based on info found in this loop + if (atom_selection_name == "O3'" and double_stranded_bool == True): + tmp_phosphate_check = _prefix + "_phosphate_check" + + if(_self.select(tmp_phosphate_check, f"{prev_oppo_res} and name P ") == 0): + _self.select("pk1", f"{prev_oppo_res} and name O5'") + _self.select("pk2", f"{prev_oppo_res} and name O5'") # This is so attach frag has a pk2 + + attach_O5_phosphate() + + _self.unpick() + _self.select("pk1",original_sele) + _self.select(tmp_connect,f"{selection}") + _self.select(prev_oppo_res, f"byres ({prev_oppo_res})") + + if (_self.select(tmp_phosphate_check, f"{prev_oppo_res} and name P")==1): + print("Phosphate has been successfully added") + + # Move the fragment + move_new_res(frag_string, tmp_editor, last_resi_sele, prev_oppo_res, double_stranded_bool, nascent.form, antisense=reverse) + + # Alter residues created + _self.alter("_tmp_editor_newresA",f"chain='{chain}';segi='{chain}';resv={resv}") + _self.alter("_tmp_editor_newresB",f"chain='{chain_oppo}';segi='{chain_oppo}';resv={resv_oppo}") + + # Connect the created residues + if double_stranded_bool == True: + bond_double_stranded(tmp_editor, object, chain, chain_oppo, resv, resv_oppo, last_resi_sele, prev_oppo_res, + atom_selection_name, atom_name_oppo) + elif double_stranded_bool == False: + bond_single_stranded(tmp_editor, object, chain, resv, last_resi_sele, atom_selection_name, atom_name_oppo) + else: + raise pymol.CmdException("double_stranded_bool is not returning True or False") + + if nascent.nuc_type == "RNA": + add2pO(tmp_domain, nascent.nuc_acid, resv) + + new_term = _prefix + "_new_selection" + _self.select(new_term, f"{object} & chain {chain} & resi \\{resv} & name {atom_selection_name}") + #_self.edit(new_term) + def fab(input,name=None,mode='peptide',resi=1,chain='',segi='',state=-1, dir=1,hydro=-1,ss=0,async_=0,quiet=1,_self=cmd, **kwargs): ''' @@ -422,6 +1077,73 @@ def fab(input,name=None,mode='peptide',resi=1,chain='',segi='',state=-1, r = DEFAULT_SUCCESS return r +def fnab(input, name=None, mode="DNA", form="B", dbl_helix=1, *, _self=cmd): + """ +DESCRIPTION + + Builds a nucleotide acid from sequence + + Fragments provided by: + Lu, Xiang-Jun, Olson, Wilma K. 3DNA: a software package for the analysis, + rebuilding and visualization of three-dimensional nucleic acid structures, + Nucleic Acids Research, 32, W667-W675 (2004). + +USAGE + + fnab input [, name [, type [, form [, dbl_helix ]]]] + +ARGUMENTS + + input = str: Sequence as an array of one letter codes + + name = str: Name of the object to create {default: obj} + + mode = str: "DNA" or "RNA" + + form = str: "A" or "B" + + dbl_helix = bool (0/1): flag for using double helix in DNA + +EXAMPLE + + fnab ATGCGATAC + fnab ATGCGATAC, name=myDNA, mode=DNA, form=B, dbl_helix=1 + fnab AAUUUUCCG, mode=RNA + """ + _self.unpick() + + if name is None: + name = _self.get_unused_name(prefix="obj") + elif name in _self.get_names('all'): + name = _self.get_unused_name(prefix=name) + + dbl_helix = int(dbl_helix) > 0 + + mode = mode.upper() + if mode not in ('DNA', 'RNA'): + raise pymol.CmdException("\"mode\" must be \"DNA\" or \"RNA\" only.") + if mode == "RNA" and dbl_helix != 0: + print ("Double helix RNA building is not currently supported.") + dbl_helix = 0 + + #first pass for error checking + for oneNA in input: + oneNA = oneNA.upper() + threeNA = _oneNA_to_threeNA.get(oneNA) + if threeNA is None: + raise pymol.CmdException("\"%s\" not of %s type..." % (oneNA, mode)) + + if threeNA not in _base_pair[mode]: + raise pymol.CmdException("\"%s\" not of %s type..." % (oneNA, mode)) + + for oneNA in input: + oneNA = oneNA.upper() + threeNA = _oneNA_to_threeNA[oneNA] + attach_nuc_acid(selection="?pk1", nuc_acid=threeNA, nuc_type=mode, + object=name, form=form, dbl_helix=dbl_helix) + _self.unpick() + return DEFAULT_SUCCESS + def build_peptide(sequence,_self=cmd): # legacy for aa in sequence: attach_amino_acid("pk1",_aa_codes[aa]) diff --git a/modules/pymol/keywords.py b/modules/pymol/keywords.py index 8e4ce2bd1..fb8c8d022 100644 --- a/modules/pymol/keywords.py +++ b/modules/pymol/keywords.py @@ -83,6 +83,7 @@ def get_command_keywords(self_cmd=cmd): 'extract' : [ self_cmd.extract , 0 , 0 , '' , parsing.STRICT ], 'exec' : [ self_cmd.python_help , 0 , 0 , '' , parsing.PYTHON ], 'fab' : [ self_cmd.fab , 0 , 0 , '' , parsing.STRICT ], + 'fnab' : [ self_cmd.fnab , 0 , 0 , '' , parsing.STRICT ], 'feedback' : [ self_cmd.feedback , 0, 0 , '' , parsing.STRICT ], 'fetch' : [ self_cmd.fetch , 0, 0 , '' , parsing.STRICT ], 'fit' : [ self_cmd.fit , 0 , 0 , '' , parsing.STRICT ], diff --git a/testing/tests/api/build.py b/testing/tests/api/build.py index deb8c4803..ff2174278 100644 --- a/testing/tests/api/build.py +++ b/testing/tests/api/build.py @@ -3,7 +3,6 @@ """ from pymol import cmd, testing -@testing.requires('incentive') @testing.requires_version('2.3') class TestNucBuilder(testing.PyMOLTestCase): def testCanInit(self): From fb4e404202ab1c9dbdda849cb3dae6d1cc92bbbb Mon Sep 17 00:00:00 2001 From: Jarrett Johnson Date: Tue, 9 Apr 2024 12:59:46 -0400 Subject: [PATCH 5/6] 3.0.0 --- ChangeLog | 32 ++++++++++++++++++++++++++++++++ layer0/Version.h | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8461eaf27..a882c80dc 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,38 @@ CHANGE LOG ========================================================================= +2024-04-09 Jarrett Johnson, Thomas Stewart, Thomas Holder, Anton Butsev + + * 3.0.0 + + * Panels + - Nucleic Acid Builder supported + - Scenes Panel (Basic version) + - Improved APBS electrostatics plugin integrated + + * Distance/Bonding detection + - Halogen-bond detection and display + + * Python API + - `fnab` command (sequence to nucleic acid chain) + - Improved CGO builder API + - get_sasa_relative() `subsele` argument added + - new clip modes/API + - iterate() adds `explicit_valence` and `explicit_degree` + - `cell_centered` setting added + - by grid-cell background gradient `bg_gradient=2` + - look_at() added + - experimental `curve_new` / `move_on_curve` + + * Developer + - C++17 required + - pymol-testing integrated + - pytest introduced + - Github Actions CI + + * And many other fixes and small additions provided by developers + and the community... + 2021-05-10 Jarrett Johnson, Thomas Holder, Thomas Stewart * 2.5.0 diff --git a/layer0/Version.h b/layer0/Version.h index 32694e277..46f69c642 100644 --- a/layer0/Version.h +++ b/layer0/Version.h @@ -1,12 +1,12 @@ #ifndef _PyMOL_VERSION -#define _PyMOL_VERSION "2.6.0a0" +#define _PyMOL_VERSION "3.0.0" #endif /* for session file compatibility */ #ifndef _PyMOL_VERSION_int // X.Y.Z -> XYYYZZZ -#define _PyMOL_VERSION_int 2004000 +#define _PyMOL_VERSION_int 3000000 // Note: There should have never been a "double" version, it's // the least useful variant to specify a version. #define _PyMOL_VERSION_double (_PyMOL_VERSION_int / 1000000.) From 07199865816c368e6c979d7262b035f3852b5021 Mon Sep 17 00:00:00 2001 From: Jarrett Johnson Date: Tue, 9 Apr 2024 13:02:21 -0400 Subject: [PATCH 6/6] 3.1.0a0 version bump --- layer0/Version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layer0/Version.h b/layer0/Version.h index 46f69c642..33f377b92 100644 --- a/layer0/Version.h +++ b/layer0/Version.h @@ -1,5 +1,5 @@ #ifndef _PyMOL_VERSION -#define _PyMOL_VERSION "3.0.0" +#define _PyMOL_VERSION "3.1.0a0" #endif /* for session file compatibility */