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