Source code for schrodinger.application.matsci.reaction_workflow_gui_utils
"""
GUI utilities for reaction workflows.
Copyright Schrodinger, LLC. All rights reserved.
"""
from collections import namedtuple
from scipy import constants
import schrodinger
from schrodinger.application.matsci import parserutils
from schrodinger.application.matsci import anharmonic
from schrodinger.application.matsci import \
jaguar_multistage_workflow_utils as jmswfu
from schrodinger.application.matsci import jagwidgets
from schrodinger.application.matsci import mswidgets
from schrodinger.application.matsci import reaction_workflow_utils as rxnwfu
from schrodinger.application.matsci import swap_fragments_utils as sfu
from schrodinger.application.matsci import \
genetic_optimization as go
from schrodinger.Qt import QtCore
from schrodinger.ui import picking
from schrodinger.ui.qt import swidgets
maestro = schrodinger.get_maestro()
SequenceData = namedtuple('SequenceData', ['amin', 'amax', 'step', 'value'])
WAVENUMBER_UNITS = f'cm{swidgets.SUPER_MINUS1}'
def _get_representative_structure(structs):
"""
Get the representative structure from the list.
:type structs: list
:param structs: contains schrodinger.structure.Structure
:rtype: schrodinger.structure.Structure
:return: the representative structure
"""
# this is going to be the first structure given that they
# are required to be conformers
return structs[0]
[docs]class AtomCollectionFrame(swidgets.SFrame):
"""
Manage an atom collection.
"""
showStructures = QtCore.pyqtSignal()
[docs] def __init__(self,
label,
tag,
n_min,
idx_getter,
sort=True,
layout=None,
use_picker=False):
"""
Create an instance.
:type label: str
:param label: the label for the line edit
:type tag: str
:param tag: an identifying tag
:type n_min: int
:param n_min: the minimum number of indices needed
when specifying indices
:type idx_getter: function
:param idx_getter: function to get indices
:type sort: bool
:param sort: whether to sort the indices
:type layout: QLayout or None
:param layout: the layout to which this widget
will be added or None if there isn't one
:type use_picker: bool
:param use_picker: if True then use a picker rather than a define button
"""
super(AtomCollectionFrame,
self).__init__(layout=layout, layout_type=swidgets.HORIZONTAL)
self.label = label
self.tag = tag
self.n_min = n_min
self.idx_getter = idx_getter
self.sort = sort
self.use_picker = use_picker
self.layOut()
self.reset()
[docs] def layOut(self):
"""
Lay out the widgets.
"""
# indices
dator = swidgets.SPosIntListValidator(delimiter=rxnwfu.INDEX_SEPARATOR)
self.idxs_le = swidgets.SLabeledEdit(self.label,
layout=self.mylayout,
validator=dator,
stretch=False,
show_clear=True)
self.idxs_le.setMinimumWidth(150)
# define
self.define_btn = swidgets.SPushButton('Define...',
layout=self.mylayout,
command=self._defineIndices)
# pick
pick_text = 'Pick atoms'
self.pick_cb = swidgets.SCheckBox(pick_text,
checked=False,
layout=self.mylayout,
command=self._pickStateChanged)
natoms = 1
self.pick_toggle = picking.PickAtomsToggle(self.pick_cb,
natoms,
self._processPickedAtom,
pick_text=pick_text)
self.define_btn.setVisible(not self.use_picker)
self.pick_cb.setVisible(self.use_picker)
def _pickStateChanged(self):
"""
React to a change in pick state.
"""
state = self.pick_cb.isChecked()
if state:
self.showStructures.emit()
def _processPickedAtom(self, picked_idxs):
"""
Process picked atom.
:type picked_idxs: list(int)
:param picked_idxs: the picked indices, will only be 1
"""
if not picked_idxs:
return
idx = picked_idxs[0]
idxs = self.getIndices()
if idx not in idxs:
idxs.append(idx)
self.setIndices(idxs)
[docs] def setIndices(self, idxs):
"""
Set the indices.
:type idxs: list
:param idxs: the idxs
"""
self.idxs_le.setText(sfu.get_idxs_str(idxs, sort=self.sort))
[docs] def getIndices(self):
"""
Get the indices.
:rtype: list
:return: the indices
"""
return sfu.get_idxs(self.idxs_le)
[docs] def setStructures(self, structs):
"""
Set the structures.
:type structs: list
:param structs: contains schrodinger.structure.Structure
"""
self.structs = structs
[docs] def setOptionsFromRepresentativeStructure(self):
"""
Set GUI options according to the properties of the representative
structure.
"""
struct = _get_representative_structure(self.structs)
if not self.getIndices():
self.setIndices(self.idx_getter(struct))
[docs] def isValid(self):
"""
Validate it.
:raise: rxnwfu.InvalidInput if there is a formatting issue
"""
struct = _get_representative_structure(self.structs)
idxs = self.getIndices()
alen = len(idxs)
if idxs and alen < self.n_min:
msg = ('If {tag} atom indices are specified '
'there must be at least {n_min} of them.').format(
tag=self.tag, n_min=self.n_min)
elif idxs and max(idxs) > struct.atom_total:
msg = ('At least a single {tag} atom index is '
'greater than the number of atoms in the '
'structure.').format(tag=self.tag)
elif len(set(idxs)) < alen:
msg = ('Some {tag} atom indices are duplicated.').format(
tag=self.tag)
else:
msg = None
if msg:
raise rxnwfu.InvalidInput(msg)
def _defineIndices(self):
"""
Define the indices.
"""
maestro.invoke_picking_loss_callback()
self.showStructures.emit()
struct = _get_representative_structure(self.structs)
asl_dlg = mswidgets.DefineASLDialog(self,
show_markers=True,
struct=struct)
accept = asl_dlg.exec()
if accept != swidgets.SDialog.Accepted:
return
idxs = asl_dlg.getIndices()
if idxs:
self.setIndices(idxs)
def _setState(self):
"""
Set the state.
"""
self.define_btn.setEnabled(bool(self.structs))
self.pick_cb.setEnabled(bool(self.structs))
[docs] def reset(self):
"""
Reset it.
"""
self.structs = None
self.idxs_le.reset()
self.pick_cb.reset()
self._setState()
[docs]class ConformationalSearch(swidgets.SGroupBox):
"""
Manage conformational search options.
"""
MAX_N_CONFORMERS_LABEL = 'Maximum number of conformers:'
MIN_N_CONFORMERS_LABEL = 'Minimum number of conformers:'
[docs] def __init__(self,
*args,
show_restrain_atoms=False,
show_return_files=True,
**kwargs):
"""
Create an instance.
:type show_restrain_atoms: bool
:param show_restrain_atoms: whether to show the restrain atoms widget
:type show_return_files: bool
:param show_return_files: whether to show the return files checkbox
"""
super().__init__(*args, **kwargs)
self.forcefield_sb = mswidgets.MSForceFieldSelector(
layout=self.layout, show_when_no_choice=True, stretch=False)
self.csearch_rel_energy_sb = swidgets.SDoubleSpinBox(minimum=0.,
maximum=100.,
stepsize=1.,
value=0.,
decimals=2)
self.csearch_rel_energy_cb = swidgets.SCheckBoxWithSubWidget(
'All conformers with relative energies less than or equal to:',
self.csearch_rel_energy_sb,
layout_type=swidgets.HORIZONTAL,
checked=True,
layout=self.layout,
command=self.updateNConformersLabel,
stretch=False)
swidgets.SLabel('kcal/mol', layout=self.csearch_rel_energy_cb.mylayout)
self.csearch_rel_energy_cb.mylayout.addStretch()
self.csearch_n_conformers_sb = swidgets.SLabeledSpinBox(
'',
minimum=1,
maximum=500,
value=rxnwfu.DEFAULT_N_CONFORMERS,
stepsize=5,
layout=self.layout)
self.csearch_seed_sb = mswidgets.RandomSeedWidget(
minimum=go.CONF_SEARCH_SEED_RANGE[0],
maximum=go.CONF_SEARCH_SEED_RANGE[1],
layout=self.layout)
self.csearch_skip_eta_rotamers_cb = swidgets.SCheckBox(
'Skip eta-rotamers generation', checked=False, layout=self.layout)
n_min = 1
self.csearch_atom_restrain_f = AtomCollectionFrame(
'Indices of atoms to restrain:',
'restrain',
n_min,
rxnwfu.get_restrain_atom_idxs,
sort=True,
layout=self.layout,
use_picker=False)
self.csearch_atom_restrain_f.setVisible(show_restrain_atoms)
self.csearch_return_files_cb = swidgets.SCheckBox(
'Return conformational search job files',
checked=False,
layout=self.layout)
self.csearch_return_files_cb.setVisible(show_return_files)
self.updateNConformersLabel()
[docs] def updateNConformersLabel(self):
"""
Update the N conformers label.
"""
if self.csearch_rel_energy_cb.isChecked():
label = self.MIN_N_CONFORMERS_LABEL
else:
label = self.MAX_N_CONFORMERS_LABEL
self.csearch_n_conformers_sb.label.setText(label)
[docs] def getRelEnergy(self):
"""
Return the relative energy in kJ/mol.
:rtype: float or None
:return: the relative energy in kJ/mol or None if not using
relative energies
"""
if self.csearch_rel_energy_cb.isChecked():
rel_energy = self.csearch_rel_energy_sb.value()
rel_energy *= constants.calorie
return rel_energy
[docs] def getCmd(self):
"""
Return the command line options.
:rtype: list
:return: the command line options which are flags
and values
"""
cmd = []
if not self.isCheckable() or self.isChecked():
cmd += [
parserutils.FLAG_FORCEFIELD,
self.forcefield_sb.getSelectionForceField()
]
rel_energy = self.getRelEnergy()
if rel_energy is not None:
cmd += [rxnwfu.FLAG_PP_REL_ENERGY_THRESH, str(rel_energy)]
cmd += [
rxnwfu.FLAG_N_CONFORMERS,
str(self.csearch_n_conformers_sb.value())
]
cmd += self.csearch_seed_sb.getCommandLineFlag()
if self.csearch_skip_eta_rotamers_cb.isChecked():
cmd += [rxnwfu.FLAG_SKIP_ETA_ROTAMERS]
if self.csearch_return_files_cb.isChecked():
cmd += [rxnwfu.FLAG_RETURN_CSEARCH_FILES]
else:
cmd += [rxnwfu.FLAG_N_CONFORMERS, '0']
return cmd
[docs] def reset(self):
"""
Reset it.
"""
super().reset()
self.forcefield_sb.force_field_menu.reset()
self.csearch_rel_energy_sb.reset()
self.csearch_rel_energy_cb.reset()
self.csearch_n_conformers_sb.reset()
self.csearch_seed_sb.reset()
self.csearch_skip_eta_rotamers_cb.reset()
self.csearch_atom_restrain_f.reset()
self.csearch_return_files_cb.reset()
self.updateNConformersLabel()
[docs]class UniformSequence(swidgets.SFrame):
"""
Manage a uniform sequence using three spin boxes.
"""
[docs] def __init__(self,
label,
units,
start_data,
step_data,
num_data,
parent_layout=None):
"""
Create an instance.
:type label: str
:param label: the label
:type units: str
:param units: the units
:type start_data: SequenceData
:param start_data: the start data
:type step_data: SequenceData
:param step_data: the step data
:type num_data: SequenceData
:param num_data: the num data
:type parent_layout: QLayout
:param parent_layout: the layout to which this widget will be added
"""
super().__init__(layout_type=swidgets.HORIZONTAL, layout=parent_layout)
swidgets.SLabel(label, layout=self.mylayout)
self.start_sb = swidgets.SLabeledDoubleSpinBox('start:',
layout=self.mylayout,
minimum=start_data.amin,
maximum=start_data.amax,
stepsize=start_data.step,
value=start_data.value,
decimals=2,
nocall=True,
after_label=units,
stretch=False)
self.step_sb = swidgets.SLabeledDoubleSpinBox('step:',
layout=self.mylayout,
minimum=step_data.amin,
maximum=step_data.amax,
stepsize=step_data.step,
value=step_data.value,
decimals=2,
nocall=True,
after_label=units,
stretch=False)
self.num_sb = swidgets.SLabeledSpinBox('number of points:',
layout=self.mylayout,
minimum=num_data.amin,
maximum=num_data.amax,
stepsize=num_data.step,
value=num_data.value,
nocall=True,
stretch=False)
self.mylayout.addStretch()
[docs] def getValues(self):
"""
Return a tuple of current values.
:rtype: tuple(float, float, int)
:return: a (start, step, num) tuple
"""
return tuple(
(self.start_sb.value(), self.step_sb.value(), self.num_sb.value()))
[docs] def setEnabled(self, state):
"""
Set the enabled state.
:type state: bool
:param state: True if enabled
"""
self.start_sb.setEnabled(state)
self.step_sb.setEnabled(state)
self.num_sb.setEnabled(state)
[docs] def reset(self):
"""
Reset it.
"""
self.start_sb.reset()
self.step_sb.reset()
self.num_sb.reset()
[docs]class Jaguar(swidgets.SGroupBox):
"""
Manage Jaguar options.
"""
[docs] def __init__(self, master, title, show_temp_and_press=True, **kwargs):
"""
Create an instance.
:param `af2.JobApp` master: The panel that this groupbox belongs to
:param str title: The title for the groupbox
:type show_temp_and_press: bool
:param show_temp_and_press: whether to show the temperature
and pressure widgets
"""
super().__init__(title, **kwargs)
def keyword_validator(keywords):
rxnwfu.type_cast_jaguar_keywords(
jagwidgets.JaguarOptionsDialog.makeString(keywords),
exception_type=KeyError)
self.jaguar_options_dlg = jagwidgets.JaguarOptionsDialog(
master,
pass_optimization_keyword=False,
show_spin_treatment=True,
show_charge=False,
show_multiplicity=False,
layout=self.layout,
default_keywords=rxnwfu.DEFAULT_JAGUAR_KEYWORDS,
keyword_validator=keyword_validator)
# max imaginary frequency
after_label = f'wavenumbers ({WAVENUMBER_UNITS})'
tip = (
'Use this option to control how negative (imaginary) frequencies\n'
'are processed for minima and transition states. A value of zero\n'
'means as usual that minima must have zero negative frequenies and\n'
'transition states must have one negative frequency. A value of -X\n'
'means the following. Minima are allowed to have negative frequencies\n'
'so long as they are smaller in magnitude than X. While transition\n'
'states may have multiple negative frequencies so long as all but one\n'
'have magnitudes smaller than X.')
self.max_i_freq_sb = swidgets.SLabeledDoubleSpinBox(
'Tolerate negative (imaginary) frequencies greater than:',
layout=self.layout,
minimum=-500.,
maximum=0.,
stepsize=50.,
value=anharmonic.DEFAULT_MAX_I_FREQ,
decimals=2,
nocall=True,
after_label=after_label,
tip=tip,
stretch=True)
# temp
start_data = SequenceData(amin=0.01,
amax=1000.00,
step=10.00,
value=jmswfu.DEFAULT_TEMP_START)
step_data = SequenceData(amin=0.01,
amax=1000.00,
step=10.00,
value=jmswfu.DEFAULT_TEMP_STEP)
num_data = SequenceData(amin=1,
amax=100,
step=1,
value=jmswfu.DEFAULT_TEMP_N)
self.temp_w = UniformSequence('Temperatures:',
'K',
start_data,
step_data,
num_data,
parent_layout=self.layout)
self.temp_w.setVisible(show_temp_and_press)
# press
start_data = SequenceData(amin=0.01,
amax=1000.00,
step=1.00,
value=jmswfu.DEFAULT_PRESS_START)
step_data = SequenceData(amin=0.01,
amax=1000.00,
step=1.00,
value=jmswfu.DEFAULT_PRESS_STEP)
num_data = SequenceData(amin=1,
amax=100,
step=1,
value=jmswfu.DEFAULT_PRESS_N)
self.press_w = UniformSequence('Pressures:',
'atm',
start_data,
step_data,
num_data,
parent_layout=self.layout)
self.press_w.setVisible(show_temp_and_press)
[docs] def getCmd(self):
"""
Return the command line options.
:rtype: list
:return: the command line options which are flags
and values
"""
cmd = []
if not self.isCheckable() or self.isChecked():
# jaguar
cmd += [
rxnwfu.FLAG_JAGUAR_KEYWORDS,
self.jaguar_options_dlg.getKeywordString()
]
# max imaginary frequency
cmd += [rxnwfu.FLAG_MAX_I_FREQ, str(self.max_i_freq_sb.value())]
# temp and press
if self.temp_w.isVisible() and self.press_w.isVisible():
start, step, num = self.temp_w.getValues()
cmd += [
rxnwfu.FLAG_TEMP_START,
str(start), rxnwfu.FLAG_TEMP_STEP,
str(step), rxnwfu.FLAG_TEMP_N,
str(num)
]
start, step, num = self.press_w.getValues()
cmd += [
rxnwfu.FLAG_PRESS_START,
str(start), rxnwfu.FLAG_PRESS_STEP,
str(step), rxnwfu.FLAG_PRESS_N,
str(num)
]
return cmd
[docs] def reset(self):
"""
Reset it.
"""
super().reset()
self.jaguar_options_dlg.reset()
self.max_i_freq_sb.reset()
self.temp_w.reset()
self.press_w.reset()
[docs]def get_entries(error, included_entry=False):
"""
Return either selected entries or the included entry from the
Maestro project table provided that the entries have atoms.
:type error: function
:param error: function to call on error
:type included_entry: bool
:param included_entry: if True then consider the included entry,
if False then consider selected entries
:rtype: schrodinger.structure.Structure or
list[schrodinger.structure.Structure] or None
:return: None if any of the entries is missing atoms otherwise
a single entry when included_entry or list of entries by default
"""
# see MATSCI-9502 - in these cases we do not use input selector
# which has a built in check for atoms so do it manually here
pt = maestro.project_table_get()
if included_entry:
rows = pt.included_rows
msg = ('There are no entries included in the workspace.')
else:
rows = pt.selected_rows
msg = ('There are no entries selected in the project table.')
if not rows:
error(msg)
return
sts = []
for row in rows:
st = row.getStructure()
if not st.atom_total:
error(f'Entry {st.title} has zero atoms.')
return
sts.append(st)
if included_entry:
if len(sts) > 1:
error('Only a single entry should be included in the workspace.')
return
return sts[0]
else:
return sts