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/data/chempy/fragments/atpA.pkl b/data/chempy/fragments/atpA.pkl new file mode 100644 index 000000000..6ef22ce9d Binary files /dev/null and b/data/chempy/fragments/atpA.pkl differ diff --git a/data/chempy/fragments/atpB.pkl b/data/chempy/fragments/atpB.pkl new file mode 100644 index 000000000..75f5b424a Binary files /dev/null and b/data/chempy/fragments/atpB.pkl differ diff --git a/data/chempy/fragments/atp_ttpA.pkl b/data/chempy/fragments/atp_ttpA.pkl new file mode 100644 index 000000000..7a1403fcd Binary files /dev/null and b/data/chempy/fragments/atp_ttpA.pkl differ diff --git a/data/chempy/fragments/atp_ttpB.pkl b/data/chempy/fragments/atp_ttpB.pkl new file mode 100644 index 000000000..139270420 Binary files /dev/null and b/data/chempy/fragments/atp_ttpB.pkl differ diff --git a/data/chempy/fragments/ctpA.pkl b/data/chempy/fragments/ctpA.pkl new file mode 100644 index 000000000..a1ac5d620 Binary files /dev/null and b/data/chempy/fragments/ctpA.pkl differ diff --git a/data/chempy/fragments/ctpB.pkl b/data/chempy/fragments/ctpB.pkl new file mode 100644 index 000000000..e86fac57f Binary files /dev/null and b/data/chempy/fragments/ctpB.pkl differ diff --git a/data/chempy/fragments/ctp_gtpA.pkl b/data/chempy/fragments/ctp_gtpA.pkl new file mode 100644 index 000000000..dd25d6be2 Binary files /dev/null and b/data/chempy/fragments/ctp_gtpA.pkl differ diff --git a/data/chempy/fragments/ctp_gtpB.pkl b/data/chempy/fragments/ctp_gtpB.pkl new file mode 100644 index 000000000..ba83fe533 Binary files /dev/null and b/data/chempy/fragments/ctp_gtpB.pkl differ diff --git a/data/chempy/fragments/gtpA.pkl b/data/chempy/fragments/gtpA.pkl new file mode 100644 index 000000000..33ff99320 Binary files /dev/null and b/data/chempy/fragments/gtpA.pkl differ diff --git a/data/chempy/fragments/gtpB.pkl b/data/chempy/fragments/gtpB.pkl new file mode 100644 index 000000000..9458716ae Binary files /dev/null and b/data/chempy/fragments/gtpB.pkl differ diff --git a/data/chempy/fragments/gtp_ctpA.pkl b/data/chempy/fragments/gtp_ctpA.pkl new file mode 100644 index 000000000..db91d55f7 Binary files /dev/null and b/data/chempy/fragments/gtp_ctpA.pkl differ diff --git a/data/chempy/fragments/gtp_ctpB.pkl b/data/chempy/fragments/gtp_ctpB.pkl new file mode 100644 index 000000000..7d23aceaf Binary files /dev/null and b/data/chempy/fragments/gtp_ctpB.pkl differ diff --git a/data/chempy/fragments/ttpA.pkl b/data/chempy/fragments/ttpA.pkl new file mode 100644 index 000000000..c54d52215 Binary files /dev/null and b/data/chempy/fragments/ttpA.pkl differ diff --git a/data/chempy/fragments/ttpB.pkl b/data/chempy/fragments/ttpB.pkl new file mode 100644 index 000000000..2df8e6660 Binary files /dev/null and b/data/chempy/fragments/ttpB.pkl differ diff --git a/data/chempy/fragments/ttp_atpA.pkl b/data/chempy/fragments/ttp_atpA.pkl new file mode 100644 index 000000000..d51e37050 Binary files /dev/null and b/data/chempy/fragments/ttp_atpA.pkl differ diff --git a/data/chempy/fragments/ttp_atpB.pkl b/data/chempy/fragments/ttp_atpB.pkl new file mode 100644 index 000000000..d03f42118 Binary files /dev/null and b/data/chempy/fragments/ttp_atpB.pkl differ diff --git a/data/chempy/fragments/utpA.pkl b/data/chempy/fragments/utpA.pkl new file mode 100644 index 000000000..11ae0560e Binary files /dev/null and b/data/chempy/fragments/utpA.pkl differ 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) 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/layer0/Version.h b/layer0/Version.h index 32694e277..33f377b92 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.1.0a0" #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.) diff --git a/layer3/Executive.cpp b/layer3/Executive.cpp index a209b4098..22d0d4e93 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); } 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_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)) 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):