"""
Contains widgets for MatSci jaguar-related panels.
Copyright Schrodinger, LLC. All rights reserved.
"""
import functools
from schrodinger.application.jaguar import basis as jag_basis
from schrodinger.application.jaguar import input as jagin
from schrodinger.application.jaguar.gui.tabs import solvation_tab
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 msutils
from schrodinger.infra import mm
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import appframework
from schrodinger.ui.qt import messagebox
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt.appframework2 import af2
from schrodinger.utils import fileutils
from schrodinger.utils import preferences
SCHRODINGER_PRESET = 'Schrodinger'
DEFAULT_PRESET = 'default'
DEFAULT_STR = ' (default)'
NO_SOLVENT = 'None'
# Jaguar 'solvent' keyword
SOLVENT_KEY = mm.MMJAG_SKEY_SOLVENT
# Jaguar 'isolv' keyword
MODEL_KEY = mm.MMJAG_IKEY_ISOLV
[docs]class BasisSetSelector(swidgets.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(swidgets.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 msutils.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 CompactSolventSelector(swidgets.SFrame):
"""
A single line of widgets that displays the currently chosen solvent and a
button that will open a dialog allowing a new solvent model/solvent choice.
Tracks the necessary Jaguar keywords to implement the user's choice.
"""
solventChanged = QtCore.pyqtSignal()
[docs] def __init__(self,
parent=None,
layout=None,
indent=False,
keywords=None,
**extra_args):
"""
Create a CompactSolventSelector object
Additional keyword arguments are passed on to the SolventDialog that is
opened by this widget.
:type layout: QBoxLayout
:param layout: The layout to place this widget into
:param bool indent: If true indent frame layout
:type keywords: dict
:param keywords: Dictionary of solvent-related Jaguar key/value pairs to
initialize/reset the widgets with
"""
super().__init__(parent=parent,
layout=layout,
indent=indent,
layout_type=swidgets.HORIZONTAL)
self.other_solvent_options = {}
if keywords is None:
self.keywords = {}
else:
self.keywords = keywords
self.default_keywords = self.keywords.copy()
self.label_label = swidgets.SLabel('Solvent:', layout=self.mylayout)
sname = self.getSolventName()
self.solvent_label = swidgets.SLabel(sname, layout=self.mylayout)
self.choose_btn = swidgets.SPushButton('Choose...',
command=self.chooseSolvent,
layout=self.mylayout)
self.dialog_args = extra_args
self.mylayout.addStretch()
[docs] def chooseSolvent(self):
"""
Open a dialog that lets the user choose solvent parameters (model,
solvent, solvent properties) and store the choices
"""
dialog = SolventDialog(self, keywords=self.keywords, **self.dialog_args)
dialog.keywordsChanged.connect(self.solventKeywordsChanged)
dialog.exec()
[docs] def solventKeywordsChanged(
self,
keywords,
options={}, # noqa: M511
update=False):
"""
Called when the user clicks accept on the SolventDialog
:type keywords: dict
:param keywords: A dictionary of Jaguar solvent model keywords
:param bool update: whether keywords should be updated or replaced
"""
if update:
self.keywords.update(keywords)
else:
self.keywords = keywords.copy()
self.other_solvent_options = options
self.solvent_label.setText(self.getSolventName())
self.solventChanged.emit()
[docs] def isSolventModelUsed(self):
"""
Has a solvent model been chosen?
:rtype: bool
:return: True if yes, False if no
"""
return bool(self.keywords.get(MODEL_KEY, 0))
[docs] def getSolventName(self):
"""
Get the name of the chosen solvent
:rtype: str
:return: The user-facing name of the chosen solvent, or NO_SOLVENT if no
model has been chosen
"""
if self.isSolventModelUsed():
return self.keywords[SOLVENT_KEY]
else:
return NO_SOLVENT
[docs] def getKeystring(self):
"""
Get a string containing all the keywords specified by the user's choices
:rtype: (str, str)
:return: First item is a string containing keywords that define the
user's choices. An empty string is returned if no model has been
selected. The second item is a string containing any keywords set
for solvent=other.
"""
if self.isSolventModelUsed():
keywords = self.keywords.copy()
if SOLVENT_KEY in keywords:
keywords[SOLVENT_KEY] = keywords[SOLVENT_KEY].replace(" ", '_')
skeywords = msutils.keyword_dict_to_string(keywords)
else:
skeywords = ""
oskeywords = ""
oskeywords = msutils.keyword_dict_to_string(self.other_solvent_options)
return (skeywords, oskeywords)
[docs] def reset(self):
"""
Reset all the widgets to their original values
"""
self.solventKeywordsChanged(self.default_keywords)
[docs]class SolventDialog(QtWidgets.QDialog):
"""
A Dialog that allows the user to pick a solvent model, solvent and
parameters.
Emits a keywordsChanged signal when the user clicks Accept and passes a
dictionary of the Jaguar keywords that reflect the selected settings.
"""
keywordsChanged = QtCore.pyqtSignal(dict, dict)
[docs] def __init__(self, parent, keywords=None, **extra_args):
"""
Create a SolventDialog object
Additional keyword arguments are passed to the EmbeddedSolventWidget
object
:type parent: QWidget
:param parent: The parent widget for this dialog
:type keywords: dict
:param keywords: A dictionary of jaguar key/value pairs that define the
initial solvent settings for the dialog
"""
self.master = parent
QtWidgets.QDialog.__init__(self, parent)
self.setWindowTitle('Solvent Model')
layout = swidgets.SVBoxLayout(self)
layout.setContentsMargins(6, 6, 6, 6)
if keywords is None:
self.keywords = {}
else:
self.keywords = keywords
self.solvent_widgets = EmbeddedSolventWidget(keywords=self.keywords,
**extra_args)
layout.addWidget(self.solvent_widgets)
dbb = QtWidgets.QDialogButtonBox
dialog_buttons = dbb(dbb.Save | dbb.Cancel | dbb.Help)
dialog_buttons.accepted.connect(self.accept)
dialog_buttons.rejected.connect(self.reject)
dialog_buttons.helpRequested.connect(self.help)
layout.addWidget(dialog_buttons)
[docs] def help(self):
"""
Show the Jaguar solvent help
"""
appframework.help_dialog('JAGUAR_TOPIC_SOLVATION_FOLDER', parent=self)
[docs] def accept(self):
"""
Gather the options and emit a keyword dictionary with the
keywords/values they define, then close the dialog.
"""
keywords = self.solvent_widgets.getMmJagKeywords()
for key, value in list(keywords.items()):
if value is None:
del keywords[key]
# The solvation tab no longer allows for "other" solvents, so there are
# no other solvent options to emit
options = {}
self.keywordsChanged.emit(keywords, options)
return QtWidgets.QDialog.accept(self)
[docs]class NewPresetDialog(swidgets.SDialog):
"""
Dialog for getting the name for a new preset
"""
[docs] def layOut(self):
"""
Lay out the widgets for the dialog
"""
layout = self.mylayout
self.name_le = swidgets.SLabeledEdit('Option set name:', layout=layout)
self.keyword_lbl = swidgets.SLabel('', layout=layout)
self.keyword_lbl.setWordWrap(True)
[docs] def accept(self):
"""
Overwrite parent's accept method to check if the name is valid
"""
name = self.name_le.text()
if not name:
self.error('The name cannot be empty.')
return
if not fileutils.is_valid_jobname(name) or not name[0].isalpha():
self.error('The name should start with a letter and can only'
' contain the following special characters: "." "_" "-"')
return
if name in {SCHRODINGER_PRESET, DEFAULT_PRESET}:
self.error(f'The name cannot be "{SCHRODINGER_PRESET}" or '
f'"{DEFAULT_PRESET}".')
return
if DEFAULT_STR in name:
self.error(f'The name cannot contain "{DEFAULT_STR}".')
return
if name in self.existing_names:
msg = ('An option set with the specified name already exists. '
'Overwrite?')
if not messagebox.show_question(
parent=self, text=msg, title='Overwrite option set?'):
return
return super().accept()
[docs] @classmethod
def getName(cls, master, keywords, existing_names):
"""
Get a name from the user for the new preset
:param `QtWidget` master: The parent widget
:param str keywords: The keywords in string format
:param set existing_names: Names for existing presets
:rtype: str or None
:return: The preset name, or None if the user cancels
"""
dlg = cls(master, title='New Option Set')
dlg.keyword_lbl.setText('Keywords: ' + keywords)
dlg.existing_names = existing_names
if dlg.exec():
return dlg.name_le.text()
return None
[docs]class PresetsDialog(swidgets.SDialog):
"""
Dialog for saving and applying jaguar keyword presets and setting default
preset
"""
[docs] def __init__(self,
master,
*,
keyword_setter,
keyword_getter,
keyword_validator=None,
schrodinger_defaults_str='',
title='Jaguar Option Sets',
preferences_group='',
**kwargs):
"""
:param QDialog master: The parent dialog
:param callable keyword_setter: The function to call to set keywords.
Should take keywords in string format as a single argument
:param callable keyword_getter: The function to call to get keywords.
Should return keywords in string format
:param callable keyword_validator: The function to call to validate
keywords. Should return a `af2.validation.ValidationResult` object
:param str schrodinger_defaults_str: Default keywords for the
Schrodinger preset in string format
:param str preferences_group: The preference group to use for storing
the presets. If not provided, a name will be created from the
parent's window title. NOTE: duplicate titles will break things when
not specifying a preferences_group
:param str title: The title for the dialog
"""
std_buttons = [
QtWidgets.QDialogButtonBox.Close, QtWidgets.QDialogButtonBox.Help
]
super().__init__(master,
title=title,
standard_buttons=std_buttons,
**kwargs)
self.setWindowModality(QtCore.Qt.WindowModal)
self.keyword_setter = keyword_setter
self.keyword_getter = keyword_getter
self.keyword_validator = keyword_validator
self.schrodinger_defaults_str = schrodinger_defaults_str
self.preferences_group = preferences_group
self.setUpPreferences()
self.populateList()
size_hint = self.sizeHint()
size_hint.setWidth(320)
self.resize(size_hint)
[docs] def layOut(self):
"""
Lay out the widgets for the dialog
"""
layout = self.mylayout
hlayout = swidgets.SHBoxLayout(layout=layout)
self.presets_listw = swidgets.SListWidget(
layout=hlayout,
nocall=True,
selection_command=self.listSelectionChanged)
min_policy = QtWidgets.QSizePolicy.Minimum
button_layout = swidgets.SVBoxLayout(layout=hlayout)
self.new_btn = swidgets.SPushButton('New...',
command=self.newPreset,
layout=button_layout,
fixed=min_policy)
button_layout.addStretch()
self.apply_btn = swidgets.SPushButton('Apply',
layout=button_layout,
command=self.applyPreset,
fixed=min_policy)
self.default_btn = swidgets.SPushButton('Set as Default',
command=self.setDefaultPreset,
layout=button_layout)
self.del_btn = swidgets.SPushButton('Delete',
layout=button_layout,
command=self.deletePreset,
fixed=min_policy)
[docs] def setUpPreferences(self):
"""
Set up the preferences for the presets
"""
self.prefs = preferences.Preferences(preferences.SCRIPTS)
if not self.preferences_group:
group = self.master.windowTitle().replace(' ', '_') + '_PRESETS'
else:
group = self.preferences_group
self.prefs.beginGroup(group)
self.prefs.set(SCHRODINGER_PRESET, self.schrodinger_defaults_str)
if not self.prefs.contains(DEFAULT_PRESET):
self.prefs.set(DEFAULT_PRESET, SCHRODINGER_PRESET)
[docs] def getCustomPresetsNames(self):
"""
Get a list of custom preset names
:rtype: list
:return: list of custom preset names
"""
custom_presets = [
p for p in self.prefs.getAllPreferences()
if p not in {SCHRODINGER_PRESET, DEFAULT_PRESET}
]
# Sort case-insensitive alphabetically
custom_presets = sorted(custom_presets, key=lambda x: x.lower())
return custom_presets
[docs] def populateList(self):
"""
Populate the list with presets
"""
self.presets_listw.clear()
presets = ['Schrodinger'] + self.getCustomPresetsNames()
for preset in presets:
if preset == self.getDefaultPresetName():
preset += DEFAULT_STR
self.presets_listw.addItem(preset)
self.listSelectionChanged()
[docs] def getSelectedPreset(self):
"""
Get the selected preset in the list, removing "(default)" if it is the
default preset
:rtype: str or None
:return: The name of the selected preset or None if there's no selection
"""
selections = self.presets_listw.selectedText()
if len(selections) == 1:
return selections[0].replace(DEFAULT_STR, '')
return None
[docs] def listSelectionChanged(self):
"""
Update the widgets based on the selected preset in the list
"""
preset_name = self.getSelectedPreset()
if not preset_name:
self.apply_btn.setEnabled(False)
self.default_btn.setEnabled(False)
self.del_btn.setEnabled(False)
return
self.apply_btn.setEnabled(True)
is_default = (preset_name == self.getDefaultPresetName())
self.default_btn.setEnabled(not is_default)
is_schrodinger = (preset_name == SCHRODINGER_PRESET)
self.del_btn.setEnabled(not is_schrodinger)
[docs] def newPreset(self):
"""
Ask the user for a new preset name and save the current keywords as
a new preset
"""
# Check if the keywords are valid before saving
if self.keyword_validator:
validation_result = self.keyword_validator()
if not validation_result:
self.error(validation_result.message)
return
# Ask the user for a name
keywords = self.keyword_getter()
existing_names = set(self.getCustomPresetsNames())
name = NewPresetDialog.getName(self, keywords, existing_names)
if not name:
# The user cancelled
return
self.prefs.set(name, keywords)
self.populateList()
[docs] def setDefaultPreset(self, *, preset_name=None):
"""
Set the default preset. If preset_name is provided, set the default to
it. Otherwise use the selected preset.
:type preset_name: str or None
:param str: preset_name: The name of the preset, or None
"""
if preset_name is None:
# Called by "Set as Default" button. Use selected preset.
preset_name = self.getSelectedPreset()
self.prefs.set(DEFAULT_PRESET, preset_name)
self.populateList()
# Select the default preset
self.presets_listw.setTextSelected(preset_name + DEFAULT_STR)
[docs] def applyPreset(self):
"""
Apply the selected preset
"""
preset_name = self.getSelectedPreset()
self.keyword_setter(self.prefs.get(preset_name))
[docs] def deletePreset(self):
"""
Delete the selected preset after confirming with the user
"""
preset_name = self.getSelectedPreset()
msg = f'Are you sure you want to delete the "{preset_name}" option set?'
if not messagebox.show_question(
parent=self, text=msg, title='Delete option set?'):
return
if preset_name == self.getDefaultPresetName():
# Set default to Schrodinger
self.setDefaultPreset(preset_name=SCHRODINGER_PRESET)
self.prefs.remove(preset_name)
self.populateList()
[docs] def getDefaultPresetName(self):
"""
Get the name of the current default preset
:rtype: str
:return: The name of the current default
"""
return self.prefs.get(DEFAULT_PRESET)
[docs] def getDefaultKeywords(self):
"""
Get the keywords for the default preset
:rtype: str
:return: The keywords for the default preset
"""
return self.prefs.get(self.getDefaultPresetName())
[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
]
ALL_SOLVENT_KEYWORDS = {
MODEL_KEY, SOLVENT_KEY, mm.MMJAG_SKEY_PCM_MODEL,
mm.MMJAG_SKEY_PCM_RADII, mm.MMJAG_IKEY_PBF_AFTER_PCM,
mm.MMJAG_IKEY_NOGAS
}
panelLabelChanged = QtCore.pyqtSignal(str)
[docs] def __init__(self,
master,
button_label='Jaguar Options...',
title=None,
help_topic=None,
preference_group='',
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,
solvent_kwargs=None,
keyword_validator=None,
default_keywords=''):
"""
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 str preferences_group: The preference group to use for storing
the presets. If not provided, a name will be created from this
dialog's title. NOTE: duplicate titles will break things when not
specifying a preferences_group
: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
:type solvent_kwargs: None or dict
:param solvent_kwargs: The kwargs to be passed to the solvent widget.
If None, the widget won't be created. An empty dict can be passed to
create the solvent widget with default kwargs.
:param callable keyword_validator: Optional function to call to validate
the keywords. Should raise KeyError if there are any issues with
the keywords.
:type default_keywords: str or dict
:param default_keywords: The default keywords for the dialog
"""
self.preference_group = preference_group
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.solvent_kwargs = solvent_kwargs
self.keyword_validator = keyword_validator
self.schrodinger_defaults = default_keywords
# 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 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)
self.setUpDefaults()
[docs] def layOut(self):
"""
Lay out the widgets for the dialog
"""
layout = self.mylayout
top_layout = swidgets.SHBoxLayout(layout=layout)
selector_frame = swidgets.SFrame(layout=top_layout)
top_right_layout = swidgets.SVBoxLayout(layout=top_layout)
swidgets.SPushButton('Option Sets...',
layout=top_right_layout,
command=self.showPresets)
top_right_layout.addStretch(10)
selector_frame.setContentsMargins(0, 15, 0, 0)
self.theory_selector = TheorySelector('Theory:',
self.DEFAULT_THEORY,
layout=selector_frame.mylayout)
self.basis_selector = BasisSetSelector('Basis set:',
self.DEFAULT_BASIS_SET,
layout=selector_frame.mylayout)
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:
extra_kwargs = {}
if self.optional_optimization:
extra_kwargs = {'checkable': True, 'checked': False}
self.geopt_gb = swidgets.SGroupBox('Geometry optimization',
parent_layout=layout,
**extra_kwargs)
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.makeDict(self.schrodinger_defaults).get(
mm.MMJAG_IKEY_ISYMM, mm.MMJAG_ISYMM_FULL)
self.symmetry_cb.setEnabled(int(symm_keyword) != mm.MMJAG_ISYMM_OFF)
if self.solvent_kwargs is not None:
# Default keywords should not be supplied in kwargs, otherwise
# resetting the solvent selector and then updating the keywords in
# updateWidgets() will cause a mix of old and new keywords
assert 'keywords' not in self.solvent_kwargs
self.solvent_selector = CompactSolventSelector(
layout=layout, **self.solvent_kwargs)
self.solvent_selector.solventChanged.connect(self.updatePanelLabel)
else:
self.solvent_selector = None
self.additional_keywords_le = KeywordEdit('Additional keywords:',
layout=layout,
stretch=False)
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 self.solvent_selector:
keywords_str = self.solvent_selector.getKeystring()[0]
keywords.update(self.makeDict(keywords_str))
if add_additional:
keywords.update(self.additional_keywords_le.getKeywordDict())
return keywords
[docs] def setUpDefaults(self):
"""
Create a `PresetsDialog` instance for this dialog and set and validate
default keywords
"""
def getter():
return self.makeString(self._getWidgetKeywords())
# save_as_current=False ensures cancelling after applying will undo
# the changes
setter = functools.partial(self.setKeywords, save_as_current=False)
self.presets_dlg = PresetsDialog(
self,
keyword_getter=getter,
keyword_setter=setter,
keyword_validator=self.validate,
schrodinger_defaults_str=self.makeString(self.schrodinger_defaults),
preferences_group=self.preference_group,
help_topic='JAGUAR_OPTIONS_PRESETS')
# Update widgets with default keywords from PresetsDialog
self.reset()
# Validate default keywords
validation_results = self.validate()
if validation_results.message:
self.error(validation_results.message)
[docs] def restrictJaguarTheoryOptions(self, text):
"""
Restrict the level of theory option in jaguar dialog box by text.
Text should be one of the predefined options. Acceptable text
values are "Recommended", "Long range corrected DFT", "Hybrid DFT",
"Meta GGA DFT", "GGA DFT", "LDA DFT"
:type text: str
:param text: specifier to select level of theory.
"""
jag_tool_btn = self.theory_selector.tool_btn
popup = jag_tool_btn._pop_up._predefined_filters_pop_up
assert text in (cbs.text() for cbs in popup._filter_cbs)
for cbs in popup._filter_cbs:
if cbs.text() == text:
cbs.setChecked(True)
cbs.setEnabled(False)
popup.filter_group_box.setCheckable(False)
[docs] def setEditEnabled(self, state):
"""
Set the enabled state of the edit button in the panel and the label
after it
:param bool state: Whether the widgets should be enabled
"""
self.panel_edit_btn.setEnabled(state)
self.panel_lbl.setEnabled(state)
[docs] def updatePanelLabel(self):
"""
Update the label in the panel with the new options
"""
parts = []
if self.solvent_selector:
solvent_name = self.solvent_selector.getSolventName()
if solvent_name != NO_SOLVENT:
parts.append(solvent_name)
parts.append(self.theory_selector.getSelection())
parts.append(self.basis_selector.getSelection())
new_text = '/'.join(parts)
self.panel_lbl.setText(new_text)
self.panelLabelChanged.emit(new_text)
[docs] def showForEdit(self):
"""
Show the dialog
"""
self.show()
self.raise_()
[docs] def showPresets(self):
"""
Show the presets dialog for this dialog
"""
self.presets_dlg.show()
self.presets_dlg.raise_()
[docs] def setKeywords(self, keywords, save_as_current=True):
"""
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
:param bool save_as_current: Whether the keywords should be saved so
cancelling the dialog doesn't undo the changes
"""
self.reset(keywords=self.makeDict(keywords),
save_as_current=save_as_current)
[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, save_as_current=True):
"""
Reset the dialog and update the widgets with any passed keywords
:type keywords: dict or None
:param keywords: The keywords to update widgets with
:param bool save_as_current: Whether the keywords should be saved so
cancelling the dialog doesn't undo the changes
"""
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()
if self.solvent_selector:
self.solvent_selector.reset()
self.additional_keywords_le.reset()
if keywords is None:
keywords = self.makeDict(self.presets_dlg.getDefaultKeywords())
self.current_keywords = keywords
elif save_as_current:
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 = msutils.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 = msutils.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}'