Source code for schrodinger.application.matsci.jagwidgets

"""
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 EmbeddedSolventWidget(solvation_tab.SolvationTab): """ A master widget that contains the widgets from the Jaguar Solvation tab and is convenient to use outside the Jaguar gui environment. """
[docs] def __init__(self, parent=None, layout=None, dielectric=True, reference=False, keywords=None, models=None): """ Create a EmbeddedSolventWidget object :type parent: QWidget :param parent: The parent widget for this widget :type layout: QBoxLayout :param layout: The layout to place this widget into :type dielectric: bool :param dielectric: If True, show the dielectric widgets, if False, do not :type reference: bool :param reference: If True, show the reference energy widgets, if False, do not :type keywords: dict :param keywords: The Jaguar solvent-related key/value pairs that define the initial widget values :type models: dict :param models: The allowed solvent models. keys are strings displayed to the user, values are Jaguar keywords. See parent class SOLVENT_MODELS constant for example. Use an OrderedDict to control the order of the solvent models in the model combobox """ if models: self.SOLVENT_MODELS = models self.SHOW_DIELECTRIC = dielectric self.reference = reference super().__init__(parent) if not self.reference: self.ui.gas_phase_frame.hide() self.layout().setContentsMargins(0, 0, 0, 0) if layout is not None: layout.addWidget(self) self.loadSettingsFromKeywords(keywords) self.default_keywords = keywords
def _getGasPhaseKeywords(self): """ Overrides the parent method to return an empty dictionary if the reference energy widgets are not shown """ if self.reference: return solvation_tab.SolvationTab._getGasPhaseKeywords(self) else: return {}
[docs] def solventModelChanged(self): super().solventModelChanged() if not self.reference: self.ui.gas_phase_frame.hide()
[docs] def loadSettingsFromKeywords(self, keywords): """ Set the widget states based on the given keyword dictionary :type keywords: dict :param keywords: Keys are jaguar keywords, values are keyword values """ jaginput = jagin.JaguarInput(genkeys=keywords) self.loadSettings(jaginput)
[docs] def reset(self): """ Reset the widgets to their initial values """ self.loadSettingsFromKeywords(self.default_keywords)
[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)
[docs] def updateWidgets(self, keyword_dict): """ Update the widgets to match the current keywords :param dict keyword_dict: The keywords to update widgets with :rtype: bool :return: True if everything was OK, False if something went wrong """ additional_keywords = {} solvent_keywords = {} for key, val in keyword_dict.items(): key = key.lower() if key == mm.MMJAG_SKEY_DFTNAME: try: self.theory_selector.setTheory(val) except ValueError: self.error(f'"{val}" is not a valid theory.') return False elif key == mm.MMJAG_SKEY_BASIS: try: self.basis_selector.setBasis(val) except ValueError: self.error(f'"{val}" is not a valid basis set.') return False elif (key == mm.MMJAG_IKEY_IUHF and self.show_spin_treatment and int(val) in self.UHF_LABELS): self.spin_treatment_rbg.setTextChecked( self.UHF_LABELS[int(val)]) elif key == mm.MMJAG_IKEY_NOPS and int(val) in (mm.MMJAG_NOPS_ON, mm.MMJAG_NOPS_OFF): # Set "Fully analytic" accuracy if the value is ON, or set # default (non-analytical) accuracy if it is OFF analytic = scf_tab.ScfTab.FULLY_ANALYTIC_ACCURACY if int(val) == mm.MMJAG_NOPS_ON: self.scf_accuracy_combo.setCurrentData(analytic) elif self.scf_accuracy_combo.currentData() == analytic: self.scf_accuracy_combo.reset() elif (key == mm.MMJAG_IKEY_IACC and int(val) in self.NON_ANALYTICAL_SCF_ACCURACIES): self.scf_accuracy_combo.setCurrentData(int(val)) elif key == mm.MMJAG_IKEY_MAXIT: self.scf_iterations_sb.setValue(int(val)) elif (key == mm.MMJAG_IKEY_IGEOPT and self.show_optimization and int(val) in (mm.MMJAG_IGEOPT_MIN, mm.MMJAG_IGEOPT_OFF)): if self.optional_optimization: self.geopt_gb.setChecked(int(val) == mm.MMJAG_IGEOPT_MIN) elif (key == mm.MMJAG_IKEY_MAXITG and self.show_optimization and self.show_geopt_iterations): self.geopt_iterations_sb.setValue(int(val)) elif key == mm.MMJAG_RKEY_NOPS_OPT_SWITCH and self.show_optimization: # Note: The dialog does not retain custom NOPS_OPT_SWITCH values self.use_nops_cb.setChecked((float(val) > 1)) elif (key == mm.MMJAG_IKEY_IACCG and self.show_optimization and int(val) in self.GEOPT_CONV_CRITERIA.values()): self.geopt_accuracy_combo.setCurrentData(int(val)) elif key == mm.MMJAG_IKEY_NOFAIL and int(val) in (0, 1): self.no_fail_cb.setChecked(int(val)) elif key == mm.MMJAG_IKEY_MOLCHG and self.show_charge: self.charge_sb.setValue(int(val)) elif key == mm.MMJAG_IKEY_MULTIP and self.show_multiplicity: self.multiplicity_sb.setValue(int(val)) elif key == mm.MMJAG_IKEY_ISYMM and int(val) in ( mm.MMJAG_ISYMM_FULL, mm.MMJAG_ISYMM_OFF): self.symmetry_cb.setChecked(int(val) == mm.MMJAG_ISYMM_FULL) elif self.solvent_selector and key in self.ALL_SOLVENT_KEYWORDS: solvent_keywords[key] = val else: additional_keywords[key] = val if solvent_keywords: # All solvent keywords are stored in solvent_selector, even # incorrect ones that the user specifies in "Additional keywords" # We use update=True so a single additional keyword does not # overwrite all existing solvent keywords. This requires that # updateWidgets is always called after clearing solvent keywords, or # when updating the keywords from additional keywords self.solvent_selector.solventKeywordsChanged(solvent_keywords, update=True) self.additional_keywords_le.setKeywords( keyword_dict=additional_keywords) return True
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}'