"""
Contains widgets for MatSci jaguar-related panels.
Copyright Schrodinger, LLC. All rights reserved.
"""
from schrodinger.application.jaguar import basis as jag_basis
from schrodinger.application.jaguar.gui import basis_selector
from schrodinger.application.jaguar.gui import theory_selector
from schrodinger.application.jaguar.gui.tabs import optimization_tab
from schrodinger.application.jaguar.gui.tabs import scf_tab
from schrodinger.application.matsci import jaguarworkflows
from schrodinger.infra import mm
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt.appframework2 import af2
from schrodinger.ui.qt import messagebox
from schrodinger.ui.qt import swidgets
[docs]class BasisSetSelector(SelectorWithPopUp):
"""
A frame that allows the user to specify a basis from a pop up list
"""
TOOL_BUTTON_CLASS = basis_selector.BasisSelectorFilterListToolButton
[docs] def selectionChanged(self):
"""
Set the line edit to a newly selected basis set
"""
self.selection_le.setText(str(self.tool_btn.getBasis()))
[docs] def setBasis(self, basis):
"""
Set the basis for the widget
:param str basis: The basis to set
:raise ValueError: If basis is invalid
"""
self.tool_btn.setBasis(basis)
# Use the same capitalization as the list widget (MATSCI-10004)
self.selection_le.setText(self.tool_btn.getBasis())
[docs] def reset(self):
"""
Reset the widget
"""
self.setBasis(self.default_selection)
[docs] def setStructure(self, struct):
"""
Set the structure to determine valid basis sets
:param `structure.Structure` struct: The structure to set
"""
self.tool_btn.setStructure(struct)
@af2.validator()
def validate(self):
"""
Check if the basis set is valid
:rtype: (False, msg) or True
:return: False and error message if something is wrong, True if
everything is OK
"""
if not self.tool_btn.hasAcceptableInput():
return (False, 'The specified basis set is not valid for the '
'input structure.')
return True
[docs]class TheorySelector(SelectorWithPopUp):
"""
A frame that allows the user to specify a theory from a pop up list
"""
TOOL_BUTTON_CLASS = theory_selector.DftTheorySelectorFilterListToolButton
[docs] def selectionChanged(self):
"""
Set the line edit to the newly selected theory
"""
self.selection_le.setText(str(self.tool_btn.getMethod()))
[docs] def setTheory(self, theory):
"""
Set the theory for the widget
:param str theory: The theory to set
:raise ValueError: If the theory is not valid
"""
self.clearFilters() # MATSCI-9750
valid = self.tool_btn.setMethod(theory)
if valid:
# Use the same capitalization as the list widget (MATSCI-10004)
self.selection_le.setText(self.tool_btn.getMethod())
else:
raise ValueError(f'"{theory}" is not in the '
'list of available theories.')
[docs] def reset(self):
"""
Reset the widget
"""
self.setTheory(self.default_selection)
[docs]class KeywordEdit(swidgets.SLabeledEdit):
"""
A labeled edit for displaying, editing and retrieving Jaguar keywords
"""
[docs] def __init__(self,
label_text="",
keyword_dict=None,
keyword_string="",
**kwargs):
"""
Create a KeywordEdit instance.
Any unrecognized keyword arguments are passed to the SLabeledEdit class
:type label_text: str
:param label_text: The text of the label for the KeywordEdit. By
default, there is no label.
:type keyword_dict: dict
:param keyword_dict: A dictionary of keyword/value pairs for the
KeywordEdit to display. If both keyword_dict and keyword_string are
supplied, the keyword_dict keywords appear first.
:type keyword_string: str
:param keyword_string: The string to display in the KeywordEdit. If both
keyword_dict and keyword_string are supplied, the keyword_dict keywords
appear first.
"""
if 'stretch' not in kwargs:
kwargs['stretch'] = False
swidgets.SLabeledEdit.__init__(self, label_text, **kwargs)
if not label_text:
self.label.hide()
self.setKeywords(
keyword_dict=keyword_dict, keyword_string=keyword_string)
self.default_text = str(self.text())
# This moves the text cursor to the beginning of the line
# of QtWidgets.QLineEdit
self.home(True)
[docs] def getKeywordString(self):
"""
Return the keyword string in the QLineEdit
:rtype: str
:return: The string in the QLineEdit. No validity checking is done.
"""
return str(self.text().lower())
[docs] def getKeywordDict(self, keystring=None):
"""
Return a dictionary whose keys are keywords and values are keyword
values
:type keystring: str
:param keystring: If provided, the keywords are taken from this string
rather than the QLineEdit. The default is to take the keywords from the
QLineEdit
:rtype: dict
:return: Dictionary of keyword/value pairs
:raise ValueError: if any tokens do not match the keyword=value format
"""
if keystring is None:
keystring = self.getKeywordString()
return jaguarworkflows.keyword_string_to_dict(keystring)
[docs] def setKeywords(self, keyword_dict=None, keyword_string=""):
"""
Set the text of the KeywordEdit
:type keyword_dict: dict
:param keyword_dict: A dictionary of keyword/value pairs for the
KeywordEdit to display. If both keyword_dict and keyword_string are
supplied, the keyword_dict keywords appear first.
:type keyword_string: str
:param keyword_string: The string to display in the KeywordEdit. If both
keyword_dict and keyword_string are supplied, the keyword_dict keywords
appear first.
"""
if keyword_dict:
keyword_text = ' '.join(
['%s=%s' % (x, y) for x, y in keyword_dict.items()])
else:
keyword_text = ""
keyword_string = keyword_string + keyword_text
self.setText(keyword_string)
[docs] def validateKeywords(self, emptyok=False):
"""
Validate the contents to ensure they are valid Jaguar keywords. The
return value of this is compatible with appframework2 validation
methods - i.e. an af2 validation method can just call:
return self.keyword_le.validateKeywords()
:type emptyok: bool
:param emptyok: Whether it is OK for the keyword input to be empty
:rtype: True or (False, str)
:return: True if no errors are found, otherwise a tuple containing False
and an error message.
"""
keywords = self.getKeywordString()
if not emptyok and not keywords:
return (False, 'Keyword input must not be empty')
return True
[docs]class JaguarOptionsDialog(swidgets.SDialog):
OVERWRITE_QUESTION_KEY = 'JAGOPTS_OVERWRITE_KEYWORDS'
SPIN_RESTRICTED = 'Restricted'
SPIN_UNRESTRICTED = 'Unrestricted'
UHF_LABELS = {
mm.MMJAG_IUHF_ON: SPIN_UNRESTRICTED,
mm.MMJAG_IUHF_OFF: SPIN_RESTRICTED
}
DEFAULT_THEORY = 'B3LYP-D3'
DEFAULT_BASIS_SET = '6-31G**'
NON_ANALYTICAL_SCF_ACCURACIES = [
val for val in scf_tab.ScfTab.ACCURACY_LEVELS.values()
if val != scf_tab.ScfTab.FULLY_ANALYTIC_ACCURACY
]
GEOPT_CONV_CRITERIA = {
x: y
for x, y in
optimization_tab.OptimizationTab.CONVERGENCE_CRITERIA.items()
if y != mm.MMJAG_IACCG_CUSTOM
}
OPTIMIZATION_KEYWORDS = [
mm.MMJAG_IKEY_IGEOPT, mm.MMJAG_IKEY_MAXITG, mm.MMJAG_IKEY_IACCG,
mm.MMJAG_RKEY_NOPS_OPT_SWITCH
]
theoryBasisChanged = QtCore.pyqtSignal(str)
[docs] def __init__(self,
master,
button_label='Jaguar Options...',
title=None,
help_topic=None,
layout=None,
show_optimization=True,
optional_optimization=False,
pass_optimization_keyword=True,
show_geopt_iterations=True,
show_spin_treatment=False,
show_charge=True,
show_multiplicity=True,
keyword_validator=None,
initial_keywords=None):
"""
Create a JaguarOptionsDialog instance
:param QWidget master: The parent widget
:param str button_label: The label for the button in the panel
:param str title: The dialog title. If not provided, a title will be
created from the panel's title
:param str help_topic: The help topic for this dialog. If not provided,
an id will be created from the panel's id
:param QLayout layout: The layout to add the panel widgets to
:param bool show_optimization: Whether geometry optimization group
should be shown
:param bool optional_optimization: Whether geometry optimization is
optional
:param bool pass_optimization_keyword: Whether geometry optimization
keyword (igeopt) should be returned when getting current keywords
:param bool show_geopt_iterations: Whether maximum iterations for
geometry optimization should be shown
:param bool show_spin_treatment: Whether spin treatment rbg should be
shown
:param bool show_charge: Whether charge spinbox should be shown
:param bool show_multiplicity: Whether multiplicity spinbox should be
shown
:param callable keyword_validator: Optional function to call to validate
the keywords. Should raise KeyError if there are any issues with
the keywords.
:type initial_keywords: str or dict
:param initial_keywords: The initial keywords for the dialog
"""
self.show_optimization = show_optimization
self.optional_optimization = optional_optimization
self.pass_optimization_keyword = pass_optimization_keyword
self.show_geopt_iterations = show_geopt_iterations
self.show_spin_treatment = show_spin_treatment
self.show_charge = show_charge
self.show_multiplicity = show_multiplicity
self.keyword_validator = keyword_validator
# Create the button and label in the panel. The label should be created
# before layOut is called
hlayout = swidgets.SHBoxLayout(layout=layout)
self.panel_edit_btn = swidgets.SPushButton(
button_label, layout=hlayout, command=self.showForEdit)
self.panel_lbl = swidgets.SLabel('', layout=hlayout)
hlayout.addStretch()
if initial_keywords is None:
initial_keywords = {}
self.initial_keywords = self.makeDict(initial_keywords)
if title is None:
title = 'Jaguar Options - ' + master.title
if help_topic is None:
help_topic = master.help_topic + '_JAGUAR_OPTIONS'
dbb = QtWidgets.QDialogButtonBox
buttons = [dbb.Ok, dbb.Cancel]
super().__init__(
master,
standard_buttons=buttons,
title=title,
help_topic=help_topic)
self.setWindowModality(QtCore.Qt.WindowModal)
[docs] def layOut(self):
"""
Lay out the widgets for the dialog
"""
layout = self.mylayout
self.theory_selector = TheorySelector(
'Theory:', self.DEFAULT_THEORY, layout=layout)
self.basis_selector = BasisSetSelector(
'Basis set:', self.DEFAULT_BASIS_SET, layout=layout)
for selector in (self.theory_selector, self.basis_selector):
selector.selection_le.textChanged.connect(self.updatePanelLabel)
self.scf_gb = swidgets.SGroupBox('SCF', parent_layout=layout)
if self.show_spin_treatment:
self.spin_treatment_rbg = swidgets.SLabeledRadioButtonGroup(
group_label="Spin treatment:",
labels=self.UHF_LABELS.values(),
layout=self.scf_gb.layout)
self.scf_accuracy_combo = swidgets.SLabeledComboBox(
'Accuracy level:',
itemdict=scf_tab.ScfTab.ACCURACY_LEVELS,
layout=self.scf_gb.layout)
self.scf_iterations_sb = swidgets.SLabeledSpinBox(
'Maximum iterations:',
minimum=1,
value=48,
maximum=999999999,
layout=self.scf_gb.layout
) # Default, min and max are from scf_tab.ui
if self.show_optimization:
self.geopt_gb = swidgets.SGroupBox(
'Geometry optimization',
parent_layout=layout,
checkable=self.optional_optimization)
if self.show_geopt_iterations:
self.geopt_iterations_sb = swidgets.SLabeledSpinBox(
'Maximum steps:',
value=100,
maximum=999999999,
layout=self.geopt_gb.layout)
self.use_nops_cb = swidgets.SCheckBox(
'Switch to analytic integrals near convergence',
layout=self.geopt_gb.layout)
self.geopt_accuracy_combo = swidgets.SLabeledComboBox(
'Convergence criteria:',
itemdict=self.GEOPT_CONV_CRITERIA,
layout=self.geopt_gb.layout)
self.no_fail_cb = swidgets.SCheckBox(
'Use special measures to prevent convergence failure',
checked=False,
layout=layout)
if self.show_charge:
self.charge_sb = swidgets.SLabeledSpinBox(
'Charge:', value=0, minimum=-99, maximum=99, layout=layout)
if self.show_multiplicity:
self.multiplicity_sb = swidgets.SLabeledSpinBox(
'Multiplicity:', value=1, minimum=1, maximum=99, layout=layout)
self.symmetry_cb = swidgets.SCheckBox(
'Use symmetry',
layout=layout,
checked=True,
disabled_checkstate=False)
# Disable the symmetry checkbox if the panel sets the keyword to 0
symm_keyword = self.initial_keywords.get(mm.MMJAG_IKEY_ISYMM,
mm.MMJAG_ISYMM_FULL)
self.symmetry_cb.setEnabled(int(symm_keyword) != mm.MMJAG_ISYMM_OFF)
self.additional_keywords_le = KeywordEdit(
'Additional keywords:', layout=layout, stretch=False)
self.reset() # Update widgets with initial keywords
validation_results = self.validate() # Validate initial keywords
if validation_results.message:
self.error(validation_results.message)
def _getWidgetKeywords(self, add_additional=True):
"""
Get the current widget keywords
:param bool add_additional: Whether additional keywords should also be
added
:rtype: dict
:return: The widget keywords
"""
# Theory and basis
keywords = {
mm.MMJAG_SKEY_DFTNAME: self.theory_selector.getSelection(),
mm.MMJAG_SKEY_BASIS: self.basis_selector.getSelection()
}
# SCF
if self.show_spin_treatment:
if self.spin_treatment_rbg.checkedText() == self.SPIN_UNRESTRICTED:
keywords[mm.MMJAG_IKEY_IUHF] = mm.MMJAG_IUHF_ON
else:
keywords[mm.MMJAG_IKEY_IUHF] = mm.MMJAG_IUHF_OFF
accuracy = self.scf_accuracy_combo.currentData()
if accuracy == scf_tab.ScfTab.FULLY_ANALYTIC_ACCURACY:
keywords[mm.MMJAG_IKEY_NOPS] = mm.MMJAG_NOPS_ON
else:
keywords[mm.MMJAG_IKEY_NOPS] = mm.MMJAG_NOPS_OFF
keywords[mm.MMJAG_IKEY_IACC] = accuracy
keywords[mm.MMJAG_IKEY_MAXIT] = self.scf_iterations_sb.value()
# Geometry optimization
if self.show_optimization and (not self.optional_optimization or
self.geopt_gb.isChecked()):
if self.pass_optimization_keyword:
keywords[mm.MMJAG_IKEY_IGEOPT] = mm.MMJAG_IGEOPT_MIN
if self.show_geopt_iterations:
keywords[
mm.MMJAG_IKEY_MAXITG] = self.geopt_iterations_sb.value()
if self.use_nops_cb.isChecked():
keywords[mm.MMJAG_RKEY_NOPS_OPT_SWITCH] = \
optimization_tab.INITIAL_NOPS_VAL
keywords[mm.MMJAG_IKEY_IACCG] = \
self.geopt_accuracy_combo.currentData()
keywords[mm.MMJAG_IKEY_NOFAIL] = int(self.no_fail_cb.isChecked())
if self.show_charge:
keywords[mm.MMJAG_IKEY_MOLCHG] = self.charge_sb.value()
if self.show_multiplicity:
keywords[mm.MMJAG_IKEY_MULTIP] = self.multiplicity_sb.value()
if self.symmetry_cb.isChecked():
keywords[mm.MMJAG_IKEY_ISYMM] = mm.MMJAG_ISYMM_FULL
else:
keywords[mm.MMJAG_IKEY_ISYMM] = mm.MMJAG_ISYMM_OFF
if add_additional:
keywords.update(self.additional_keywords_le.getKeywordDict())
return keywords
[docs] def updatePanelLabel(self):
"""
Update the label in the panel with the new theory and basis
"""
new_text = (self.theory_selector.getSelection() + '/' +
self.basis_selector.getSelection())
self.panel_lbl.setText(new_text)
self.theoryBasisChanged.emit(new_text)
[docs] def showForEdit(self):
"""
Show the dialog
"""
self.show()
self.raise_()
[docs] def setKeywords(self, keywords):
"""
Set the keywords for the dialog. Widgets that are not modified by the
passed keywords will be reset to default.
:type keywords: str or dict
:param keywords: The keywords to set
"""
self.reset(keywords=self.makeDict(keywords))
[docs] def getKeywordDict(self):
"""
Get current keywords as a dict
:rtype: dict
:return: The current keywords as dict
"""
return self.current_keywords
[docs] def getKeywordString(self):
"""
Get current keywords as a string
:rtype: str
:return: The current keywords as a string
"""
return self.makeString(self.current_keywords)
[docs] def accept(self):
"""
Update the keywords and close the dialog if the inputs are valid
"""
validation_result = self.validate()
if validation_result.message:
self.error(validation_result.message)
elif validation_result:
self.current_keywords = self._getWidgetKeywords()
super().accept()
[docs] def reject(self):
"""
Close the dialog and reset it back to when it was opened
"""
super().reject()
self.reset(keywords=self.current_keywords)
[docs] def reset(self, keywords=None):
"""
Reset the dialog and update the widgets with any passed keywords
:type keywords: dict or None
:param keywords: The keywords to update widgets with
"""
self.theory_selector.reset()
self.basis_selector.reset()
if self.show_spin_treatment:
self.spin_treatment_rbg.reset()
self.scf_accuracy_combo.reset()
self.scf_iterations_sb.reset()
if self.show_optimization:
if self.optional_optimization:
self.geopt_gb.reset()
if self.show_geopt_iterations:
self.geopt_iterations_sb.reset()
self.use_nops_cb.reset()
self.geopt_accuracy_combo.reset()
self.no_fail_cb.reset()
if self.show_charge:
self.charge_sb.reset()
if self.show_multiplicity:
self.multiplicity_sb.reset()
self.symmetry_cb.reset()
self.additional_keywords_le.reset()
if keywords is None:
keywords = self.initial_keywords
self.current_keywords = keywords
self.updateWidgets(keywords)
@af2.validator()
def validate(self):
"""
Validate the dialog keywords
:rtype: bool or (bool, str)
:return: True if everything is OK, (False, msg) if the state is invalid
and an error should be shown to the user in a warning dialog.
"""
# Make sure the keyword syntax is valid
try:
additional_keywords = self.additional_keywords_le.getKeywordDict()
except ValueError as err:
return False, str(err)
# Make sure all additional keywords and values are valid
for key, val in additional_keywords.items():
if hasattr(mm, 'MMJAG_IKEY_' + key.upper()):
try:
int(val)
except ValueError:
return False, (f'The "{key}" keyword '
'requires an integer value.')
elif hasattr(mm, 'MMJAG_RKEY_' + key.upper()):
try:
float(val)
except ValueError:
return False, (f'The "{key}" keyword requires'
' a floating point value.')
elif not hasattr(mm, 'MMJAG_SKEY_' + key.upper()):
pass
# Cannot rely on the existing variables to see which keywords
# exist (MATSCI-10453)
# return False, f'"{key}" is not a Jaguar keyword.'
# yapf: disable
# Verify that all additional keywords are allowed
optional_widget_keywords = (
(self.show_optimization, self.OPTIMIZATION_KEYWORDS),
(self.pass_optimization_keyword, [mm.MMJAG_IKEY_IGEOPT]),
(self.show_geopt_iterations, [mm.MMJAG_IKEY_MAXITG]),
(self.show_charge, [mm.MMJAG_IKEY_MOLCHG]),
(self.show_multiplicity, [mm.MMJAG_IKEY_MULTIP]),
(self.show_spin_treatment, [mm.MMJAG_IKEY_IUHF])
)
# yapf: enable
for is_shown, keywords in optional_widget_keywords:
if not is_shown:
for keyword in keywords:
if keyword in additional_keywords:
return False, (f'The "{keyword}" keyword may '
'not be used with this panel.')
# Put any keywords supported by the widgets into them
if not self.updateWidgets(additional_keywords):
return False
widget_keywords = self._getWidgetKeywords(add_additional=False)
additional_keywords = self.additional_keywords_le.getKeywordDict()
# Validate keywords using the custom validator, if any
if self.keyword_validator:
all_keywords = dict(widget_keywords)
all_keywords.update(additional_keywords)
try:
self.keyword_validator(all_keywords)
except KeyError as msg:
return False, str(msg)
# Check if any of the additional keywords is overwriting widget keywords
overwrite = [k for k in additional_keywords if k in widget_keywords]
if overwrite:
msg = (
'The following additional keywords will overwrite '
'existing keywords:\n' + '\n'.join(overwrite) + '\n\nContinue?')
if not messagebox.show_question(
parent=self,
text=msg,
title='Overwrite keywords?',
save_response_key=self.OVERWRITE_QUESTION_KEY):
return False
return True
@af2.validator()
def validateBasisSet(self, structs):
"""
Validate that the passed structures are compatible with the current
basis set
:param iterable structs: The structures to validate
:rtype: bool or (bool, str)
:return: True if everything is OK, (False, msg) if the state is invalid
and an error should be shown to the user.
"""
basis_name = self.basis_selector.getSelection()
for struct in structs:
num_funcs = jag_basis.num_functions_all_atoms(basis_name, struct)[0]
if num_funcs == 0:
error = (f'The "{basis_name}" basis set is not valid'
f' for the "{struct.title}" structure.')
return False, error
return True
[docs] @staticmethod
def makeDict(keywords):
"""
Create a keyword dictionary if the passed keywords are in string format
:type keywords: str or dict
:param keywords: Keyword dict or string
:rtype: dict
:return: Keyword dictionary
"""
if isinstance(keywords, str):
keywords = jaguarworkflows.keyword_string_to_dict(keywords)
return keywords
[docs] @staticmethod
def makeString(keywords):
"""
Create a keyword string if the passed keywords are in dictionary format
:type keywords: str or dict
:param keywords: Keyword dict or string
:rtype: str
:return: Keyword string
"""
if isinstance(keywords, dict):
keywords = jaguarworkflows.keyword_dict_to_string(keywords)
return keywords
[docs] def assertInKeywords(self, keywords):
"""
Assert that the passed keywords are a subset of the dialog's keywords
Used for unittests.
:type keywords: str or dict
:param keywords: Keyword dict or string
"""
for key, val in self.makeDict(keywords).items():
assert key in self.current_keywords, (f'{key} is not in '
f'{self.current_keywords}')
actual_val = self.current_keywords[key]
if isinstance(actual_val, str) and isinstance(val, str):
actual_val = actual_val.lower()
val = val.lower()
assert val == actual_val, f'{val} != {actual_val}'