"""
Utility functions and classes for the Jaguar GUIs
"""
import enum
import math
import re
import warnings
from collections import namedtuple
import decorator
import schrodinger
from schrodinger.application.jaguar import input as jaginput
from schrodinger.infra import mm
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt.appframework2 import jobnames as af2_jobnames
maestro = schrodinger.get_maestro()
THEORY_DFT = "DFT"
THEORY_HF = "HF"
THEORY_LMP2 = "LMP2"
THEORY_MIXED = "Mixed"
THEORY_RIMP2 = "RI-MP2"
MOLECULAR_CHARGE_PROP = "i_m_Molecular_charge"
SPIN_MULT_PROP = "i_m_Spin_multiplicity"
ERROR_BACKGROUND_BRUSH = QtGui.QBrush(Qt.red)
JAGUAR_IN_MAE_FILETYPES = (("Jaguar Structure Input",
"*.in *.mae *.maegz *.mae.gz"),
("Jaguar Input", "*.in"), ("Maestro",
"*.mae *.maegz *.mae.gz"))
Solvent = namedtuple(
"Solvent",
("name", "keyvalue", "dielectric", "radius", "weight", "density"))
# "density" represents the density of the solvent at 20 C
# Names for the "Solvent" option menu and the values for the
# MMJAG_SKEY_SOLVENT "solvent" keyword
ALL_SOLVENTS = [
Solvent("Water", mm.MMJAG_SOLVENT_WATER, 80.37, 1.4, 18.02, 0.99823),
Solvent("Acetonitrile", mm.MMJAG_SOLVENT_ACETONITRILE, 37.5, 2.19, 41.05,
0.777),
Solvent("Benzene", mm.MMJAG_SOLVENT_BENZENE, 2.284, 2.60, 78.12, 0.87865),
Solvent("Carbon tetrachloride", mm.MMJAG_SOLVENT_CARBON_TETRACHLORIDE,
2.238, 2.67, 153.82, 1.5940),
Solvent("Chlorobenzene", mm.MMJAG_SOLVENT_CHLOROBENZENE, 5.708, 2.72,
112.56, 1.1058),
Solvent("Chloroform", mm.MMJAG_SOLVENT_CHLOROFORM, 4.806, 2.52, 119.38,
1.4832),
Solvent("Cyclohexane", mm.MMJAG_SOLVENT_CYCLOHEXANE, 2.023, 2.78, 84.16,
0.77855),
Solvent("1,2-dichloroethane", mm.MMJAG_SOLVENT_12DICHLOROETHANE, 10.65,
2.51, 98.96, 1.2351),
Solvent("Dichloromethane", mm.MMJAG_SOLVENT_DICHLOROMETHANE, 8.93, 2.33,
84.93, 1.3266),
Solvent("Dimethylformamide", mm.MMJAG_SOLVENT_DIMETHYLFORMAMIDE, 36.7, 2.49,
73.09, 0.944),
Solvent("DMSO", mm.MMJAG_SOLVENT_DMSO, 36.7, 2.41, 78.13, 1.1014),
Solvent("Ethanol", mm.MMJAG_SOLVENT_ETHANOL, 24.85, 2.27, 46.07, 0.785),
Solvent("Methanol", mm.MMJAG_SOLVENT_METHANOL, 33.62, 2.00, 32.04, 0.7914),
Solvent("Nitrobenzene", mm.MMJAG_SOLVENT_NITROBENZENE, 35.74, 2.73, 123.11,
1.2037),
Solvent("Tetrahydrofuran", mm.MMJAG_SOLVENT_TETRAHYDROFURAN, 7.6, 2.52,
72.11, 0.8892),
Solvent("Other", "other", None, None, None, None),
]
[docs]def count_num_strucs(input_selector):
"""
Count the number of structures currently specified in the input selector
widget. Since Jaguar can't accept more than three structures, this function
will return 4 for all values >= 4.
:param input_selector: The input selector widget
:type input_selector: `schrodinger.ui.qt.input_selector.InputSelector`
"""
try:
strucs = input_selector.structures(False)
for num_strucs in range(0, 5):
next(strucs)
except StopIteration:
pass
except IOError:
return 0
return num_strucs
[docs]class JaguarSettingError(Exception):
"""
An exception indicating that there was an error with the user-specified
Jaguar settings
"""
# This class intentionally left blank
[docs]class JaguarSettingWarning(UserWarning):
"""
A warning indicating that a user-specified Jaguar settings could not be
loaded into the GUI
"""
# This class intentionally left blank
[docs]def find_key_for_value(mydict, value):
"""
This function finds key corresponding to a given value in a
dictionary. We assume here that values in a given dictionary
are unique.
:param mydict: dictionary
:type mydict: dict
:param value: value in dictionary. It can be any hashable type.
:return: key, which can be any type.
"""
return next((k for k, v in mydict.items() if v == value), None)
[docs]@decorator.decorator
def catch_jag_errors(func, *args, **kwargs):
"""
A decorator that will display any `JaguarSettingError` instances in an
error dialog.
:return: If the decorated function raised a `JaguarSettingError`, False
will be returned. Otherwise, the return value of the decorated function
will be returned.
"""
self = args[0]
try:
return func(*args, **kwargs)
except JaguarSettingError as err:
if str(err):
self.error(str(err))
return False
[docs]def warn_about_mmjag_unknowns(jag_input, parent=None):
"""
If the JaguarInput object contains any unknown keywords, warn the user about
the unknowns and ask them if they want to continue.
:param jag_input: The JaguarInput object to check for unknown keywords
:type jag_input: schrodinger.application.jaguar.input.JaguarInput
:param parent: The Qt parent widget for the warning dialog
:type parent: QtWidgets.QWidget
:return: True if we should continue (either there were no unknown keywords,
or there were but the user wants to continue). False if we should
cancel.
:rtype: bool
"""
unknowns = jag_input.getUnknownKeywords()
if unknowns:
plurals = ("is an", "") if len(unknowns) == 1 else ("are", "s")
err = ("There %s unrecognized keyword%s in the input file:" % plurals)
for (key, val) in sorted(unknowns.items()):
err += "\n\t%s=%s" % (key, val)
plurals = ("this", "") if len(unknowns) == 1 else ("these", "s")
err += ("\nDo you still wish to continue with %s unrecognized "
"keyword%s?" % plurals)
QMB = QtWidgets.QMessageBox
msg_box = QMB(QMB.Warning, "Warning", err, QMB.Ignore | QMB.Cancel,
parent)
msg_box.button(QMB.Ignore).setText("Ignore and Continue")
retval = msg_box.exec()
return retval == QMB.Ignore
else:
return True
[docs]def calculate_num_protons(struc):
"""
Calculate the number of protons in the specified structure
:param struc: The structure to calculate protons in
:type struc: schrodinger.structure.Structure
:return: The number of protons
:rtype: int
"""
num_protons = 0
for cur_atom in struc.atom:
counterpoise = cur_atom.property.get(mm.M2IO_DATA_ATOM_COUNTERPOISE,
False)
if not counterpoise and cur_atom.atomic_number > 0:
num_protons += cur_atom.atomic_number
return num_protons
[docs]def get_atom_info(ws_atom_num):
"""
Get information about the specified workspace atom
:param ws_atom_num: The workspace atom number
:type ws_atom_num: int
:return: A tuple of:
- The atom name (after Jaguar atom naming has been applied)
- The atom number relative to the entry (rather than relative to the
workspace structure)
- The entry id
- The entry title
:rtype: tuple
"""
ws_struc = maestro.workspace_get()
ws_atom = ws_struc.atom[ws_atom_num]
eid = ws_atom.entry_id
atom_num = ws_atom.number_by_entry
proj = maestro.project_table_get()
row = proj[eid]
struc = row.getStructure()
jaginput.apply_jaguar_atom_naming(struc)
atom = struc.atom[atom_num]
return atom.name, atom_num, eid, row.title
atom_name_regex = re.compile(r"^([^\W\d]+)(\d+)$")
"""
A regular expression that matches Jaguar atom names. Group 1 of the match is
the element and group 2 is the number.
"""
[docs]def atom_name_sort_key(atom_name):
"""
Convert a Jaguar atom name into a key for use in sorting by number
:param atom_name: The atom name
:type atom_name: str
:return: A tuple of (atom number, element)
:rtype: tuple
"""
groups = atom_name_regex.match(atom_name).groups()
num_name = (int(groups[1]), groups[0])
return num_name
[docs]def generate_job_name(struc_name, task_name, theory, basis):
"""
Generate an appropriate job name for a job with the specified settings. Any
settings that are specified as None will be omitted from the job name. If a
directory or file with the specified name exists in the current directory,
an integer will be appended to the job name.
:note: If generating multiple job names, the input for each job must be
saved before the next job name is generated. Otherwise, this function will
not be able to append the appropriate integer.
:param struc_name: The structure title
:type struc_name: str or NoneType
:param task_name: The task name (i.e. a shortened version of the panel name
:type task_name: str or NoneType
:param theory: The theory method. Should be "HF", "LMP2", or the DFT
functional.
:type theory: str or NoneType
:param basis: The basis name
:type basis: str or NoneType
:return: The job name
:rtype: str
"""
if basis:
# Replace all characters in the basis that aren't allowed in job names
basis = basis.replace("*", "s").replace("+", "p")
basis = basis.replace("(", "").replace(")", "")
if theory:
theory = theory.replace("(", "").replace(")", "")
if struc_name:
# The structure name may contain characters that aren't allowed in job
# names, so remove all characters other than alphanumerics, underscores,
# dashes, and periods. (Allowed characters taken from
# L{schrodinger.utils.fileutils.is_valid_jobname}.)
struc_name = re.sub(r"[^\w_\-\.]", "", struc_name)
job_data = ("jag", struc_name, task_name, theory, basis)
job_data = [name for name in job_data if name]
jobname = "_".join(job_data)
jobname = af2_jobnames.get_next_jobname(jobname, True)
return jobname
[docs]class EnhancedComboBox(swidgets.SComboBox):
"""
A combo box for use in the Jaguar GUI with several Pythonic enhancements
"""
[docs] def setCurrentMmJagData(self, jag_input, keyword, setting_name):
"""
Set the combo box selection based on the specified mmjag setting The
combo box user data must match the mmjag keyword values. If no matching
setting is found, a warning will be issued.
:param jag_input: A JaguarInput object containing the settings to load
:type jag_input: `schrodinger.application.jaguar.input.JaguarInput`
:param keyword: The mmjag keyword to load
:type keyword: str
:param setting_name: The name of the setting that is being set. This
name will only be used when issuing warnings.
:type setting_name: str
"""
value = jag_input[keyword]
index = self.findData(value)
if index != -1:
self.setCurrentIndex(index)
else:
msg = "Unknown value for %s: %s=%s" % (setting_name, keyword,
str(value))
warnings.warn(JaguarSettingWarning(msg))
[docs]class WorkspaceInclusionCheckBox(QtWidgets.QCheckBox):
"""
A checkbox that is skinned to look like the Project Table workspace
inclusion checkbox. This checkbox is used in the Transition State and IRC
tabs.
:note: This skinning could be done using a style sheet, but that requires
separate images for checked + disabled and unchecked + disabled. By using a
QIcon, these disabled images are generated automatically.
"""
[docs] def __init__(self, parent=None):
super(WorkspaceInclusionCheckBox, self).__init__(parent)
self._icon = QtGui.QIcon()
self._icon.addFile(":/icons/include_checked.png", state=QtGui.QIcon.On)
self._icon.addFile(":/icons/include_unchecked.png",
state=QtGui.QIcon.Off)
[docs] def paintEvent(self, event):
painter = QtGui.QPainter(self)
opt = QtWidgets.QStyleOptionButton()
self.initStyleOption(opt)
if self.isEnabled():
if opt.state & QtWidgets.QStyle.State_MouseOver:
mode = QtGui.QIcon.Active
elif opt.state & QtWidgets.QStyle.State_Selected:
mode = QtGui.QIcon.Selected
else:
mode = QtGui.QIcon.Normal
else:
mode = QtGui.QIcon.Disabled
if self.isChecked():
state = QtGui.QIcon.On
else:
state = QtGui.QIcon.Off
rect = opt.rect
align = Qt.AlignLeft | Qt.AlignVCenter
self._icon.paint(painter, rect, align, mode, state)
[docs]class ProjTableLikeView(QtWidgets.QTableView):
"""
A table view that mimics the selecting and editing behaviors of the Project
Table
"""
[docs] def __init__(self, parent=None):
super(ProjTableLikeView, self).__init__(parent)
self.setSortingEnabled(True)
self.setSelectionBehavior(self.SelectRows)
self.setSelectionMode(self.ExtendedSelection)
edit_triggers = self.editTriggers()
edit_triggers |= self.SelectedClicked
self.setEditTriggers(edit_triggers)
[docs] def selectionCommand(self, index, event=None):
"""
Don't update the current selection when using the keyboard to navigate
or when clicking on a selected editable item.
:param index: The newly selected index
:type index: `PyQt5.QtCore.QModelIndex`
:param event: The event that triggered the index change
:type event: `PyQt5.QtCore.QEvent`
:return: A flag describing how the selection should be updated
:rtype: int
"""
smodel = self.selectionModel()
keypress = event is not None and event.type() == event.KeyPress
selected = index.isValid() and smodel.isSelected(index)
editable = index.flags() & Qt.ItemIsEditable
if keypress or (selected and editable):
return smodel.NoUpdate
else:
return super(ProjTableLikeView, self).selectionCommand(index, event)
[docs] def commitDataToSelected(self, editor, index, delegate):
"""
Commit data to all selected cells in the column that is currently being
edited.
:param editor: The editor being used to enter data
:type editor: `PyQt5.QtWidgets.QWidget`
:param index: The index being edited
:type index: `PyQt5.QtCore.QModelIndex`
:param delegate: The delegate used to create the editor
:type delegate: `PyQt5.QtWidgets.QAbstractItemDelegate`
"""
all_selected = self.selectedIndexes()
indices = [
sel for sel in all_selected if sel.column() == index.column()
]
if index not in indices:
# This is possible using keyboard navigation
indices.append(index)
model = self.model()
for cur_index in indices:
delegate.setModelData(editor, model, cur_index)
[docs] def setItemDelegateForColumn(self,
column,
delegate,
connect_selected=False):
"""
Set the delegate for the specified column. Note that this function adds
the optional `connect_selected` argument not present in the QTableView
function.
:param column: The column to set the delegate for
:type column: int
:param delegate: The delegate to set
:type delegate: `PyQt5.QtWidgets.QAbstractItemDelegate`
:param connect_selected: If True, the delegate's commitDataToSelected
signal will be connected
:type connect_selected: bool
"""
if connect_selected:
delegate.commitDataToSelected.connect(self.commitDataToSelected)
super(ProjTableLikeView,
self).setItemDelegateForColumn(column, delegate)
[docs]@enum.unique
class SpinTreatment(enum.Enum):
"""
An enumeration of the possible spin treatment settings. Enum values
correspond to mmjag settings.
"""
NA = None # Spin treatment is not applicable to Hartree-Fock and lMP2
# levels of theory
Restricted = mm.MMJAG_IUHF_OFF
Unrestricted = mm.MMJAG_IUHF_ON
Automatic = mm.MMJAG_IUHF_AUTOMATIC
[docs] def unrestrictedAvailable(self):
"""
Does the current spin treatment setting allow for an unrestricted
waveform?
:return: True for unrestricted or automatic spin treatments. False
otherwise.
:rtype: bool
"""
return self in (SpinTreatment.Unrestricted, SpinTreatment.Automatic)
def __bool__(self):
err = ("SpinTreatment enums should not be treated as a Boolean. Use "
"Spintreatment.unrestrictedAvailable() instead.")
raise RuntimeError(err)