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):