Source code for schrodinger.application.jaguar.gui.basis_selector

import csv
import os

from schrodinger.application.jaguar import basis as jag_basis
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import filter_list
from schrodinger.ui.qt import pop_up_widgets
from schrodinger.ui.qt.decorators import suppress_signals
from schrodinger.ui.qt.standard.colors import LightModeColors
from schrodinger.utils import csv_unicode
from schrodinger.utils import fileutils

(REJECT, ACCEPT, ACCEPT_MULTI) = list(range(3))
NO_BASIS_MSG = "Basis set not available for every element in system."
NO_PS_MSG = "Pseudospectral grids not available for every element in system."
NO_PS_MSG_ATOMIC = "Pseudospectral grids not available for this atom type."
RELATIVISTIC_PREFIX = "Relativistic "
DYALL = "DYALL"
SARC_ZORA = "SARC-ZORA"


[docs]def num_basis_functions(basis_name, struc, per_atom=None, atom_num=None): """ Calculate the number of basis functions for the specified structure or atom. :param basis_name: The basis name including stars and pluses :type basis_name: str :param struc: The structure :type struc: `schrodinger.structure.Structure` :param per_atom: An optional dictionary of {atom index: basis name} for per-atom basis sets :type per_atom: dict or NoneType :param atom_num: The atom index in `struc` to calculate the number of basis functions for. If given, the number of basis functions will be calculated for a single atom. If not given, the number of basis functions will be calculated for the entire structure. :type atom_num: int or NoneType :return: A tuple of: - The number of basis functions for `struc` (if atom_num is not given) or for atom `atom_num` (int) - Are pseudospectral grids available for the specified structure or atom (bool) :rtype: tuple :note: Either `per_atom` or `atom_num` may be given (or neither), but not both. """ if atom_num is None: retval = jag_basis.num_functions_all_atoms(basis_name, struc, per_atom) num_funcs, is_ps, num_funcs_per_atom = retval return num_funcs, is_ps else: return jag_basis.num_functions_per_atom(basis_name, struc, atom_num)
[docs]def generate_description(basis_name, struc, per_atom=None, atom_num=None): """ Return a description of the specified basis set applied to the given structure or atom. :param basis_name: The basis set name :type basis_name: str :param struc: The structure :type struc: `schrodinger.structure.Structure` :param per_atom: An optional dictionary of {atom index: basis name} for per-atom basis sets :type per_atom: dict or NoneType :param atom_num: The atom index in `struc` to calculate the number of basis functions for. If given, the number of basis functions will be calculated for a single atom. If not given, the number of basis functions will be calculated for the entire structure. Also controls use of NO_PS_MSG vs NO_PS_MSG_ATOMIC. :type atom_num: int or NoneType :return: A list of four sentences describing the basis set. If a sentence does not apply to the basis set/structure combination, that location in the list will be None. :rtype: list """ sentences = [None] * 4 basis = jag_basis.get_basis_by_name(basis_name) if struc is None: num_funcs = 0 else: num_funcs, is_ps = num_basis_functions(basis_name, struc, per_atom, atom_num) plural_s = "" if num_funcs == 1 else "s" sentences[0] = "%s basis function%s." % (num_funcs, plural_s) if num_funcs == 0: sentences[1] = NO_BASIS_MSG elif not is_ps: if atom_num: sentences[1] = NO_PS_MSG_ATOMIC else: sentences[1] = NO_PS_MSG if basis.is_ecp: sentences[2] = "Effective core potentials on heavy atoms." if basis.backup: sentences[3] = "Non-ECP atoms use %s basis." % basis.backup return sentences, num_funcs
[docs]def combine_sentences(sentences): """ Given a list of sentences, combine all non-None sentences. :param sentences: A list of sentences, where each sentence is either a string ending in a punctuation mark or None :type sentences: list :return: The combined sentences :rtype: str """ sentences = [s for s in sentences if s is not None] return " ".join(sentences)
[docs]def get_basis_display_name(basis): """ Return the display name for the specified basis. :param basis: Basis set to get the display name for. :type basis: str :return: The display name for this basis :rtype: str """ basis_name, polarization, diffuse = jag_basis.parse_basis(basis) # PANEL-17330 Input files and Jaguar data dir differ in order # of +'s and *'s basis = basis_name + '+' * diffuse + '*' * polarization if basis.startswith(DYALL) or basis.startswith(SARC_ZORA): basis = RELATIVISTIC_PREFIX + basis return basis
[docs]def get_basis_set_list_items(): """ :return: tuple of basis set list items :rtype: tuple(BasisSetListWidgetItem) """ list_items = [] data_path = os.path.join(fileutils.get_mmshare_data_dir(), 'jaguar', 'basis_sets.csv') with csv_unicode.reader_open(data_path) as fh: reader = csv.DictReader(fh) for row in reader: for col, val in row.items(): if col != 'full_name': if val not in ("0", "1"): raise RuntimeError( f"Unexpected {col} value for {row['full_name']}: {val}" ) row[col] = val == '1' basis_display_name = get_basis_display_name(row['full_name']) item = BasisSetListWidgetItem(basis_display_name, **row) list_items.append(item) return tuple(list_items)
[docs]class BasisSelectorLineEdit(pop_up_widgets.LineEditWithPopUp): """ A line edit that can be used to select a basis set. A basis selector pop up will appear whenever this line edit has focus. """
[docs] def __init__(self, parent): super(BasisSelectorLineEdit, self).__init__(parent, _BasisSelectorPopUp) self._blank_basis_allowed = False self._acceptable_basis = False self._setUpCompleter() validator = UpperCaseValidator(parent) self.setValidator(validator)
[docs] def setBlankBasisAllowed(self, allowed): """ Specify whether a blank basis set is allowed or should result in an "Invalid basis set" warning and a red outline around the line edit. :param allowed: True if a blank basis set should be allowed. False if a blank basis set should result in a warning. :type allowed: bool """ self._blank_basis_allowed = allowed self._pop_up.setBlankBasisAllowed(allowed) if self.text() == "": self._acceptable_basis = allowed self._updateBorder()
def _setUpCompleter(self): """ Create a completer that will suggest valid basis set names """ # By using the basis model from the pop up, only basis sets that are # applicable to the current structure will be suggested completer = QtWidgets.QCompleter(self._pop_up.basisModel(), self) completer.setCaseSensitivity(Qt.CaseInsensitive) completer.setCompletionMode(completer.InlineCompletion) self.setCompleter(completer)
[docs] def textUpdated(self, text): """ Whenever the text in the line edit is changed, update the pop up and the red error outline :param text: The current text in the line edit :type text: str """ self._pop_up.show() with suppress_signals(self._pop_up): self._acceptable_basis = self._pop_up.setBasisSafe(text) self._updateBorder()
def _updateBorder(self): """ If the user has specified an invalid basis set, draw a red outline around the line edit. If the user has specified a valid basis set, set the outline back to normal. """ if self._acceptable_basis: self.setStyleSheet("") else: # We have to hardcode the border width when using a style sheet, # which may lead to issues if the user has a dramatically different # width set. Overriding paintEvent() may be the only way to fix # this, though, so it's being left as is for now. self.setStyleSheet( f"border: 2px solid {LightModeColors.INVALID_STATE_BORDER}")
[docs] def popUpUpdated(self, basis_name): """ Whenever the basis selected in the pop up is changed, update the line edit. :param basis_name: The basis selected in the pop up :type basis_name: str """ with suppress_signals(self): self.setText(basis_name) self._acceptable_basis = True self._updateBorder()
[docs] def setStructure(self, struc): """ Set the structure to use for determining basis set information and availability :param struc: The structure to use :type struc: `schrodinger.structure.Structure` """ self._pop_up.setStructure(struc)
[docs] def setStructureAndUpdateBorder(self, struct): """ Set the structure and update the edit border to show whether the current basis set is valid for the structure or not :param struct: The structure to use :type struct: `schrodinger.structure.Structure` """ self.setStructure(struct) self._acceptable_basis = self._pop_up.isValid() self._updateBorder()
[docs] def setAtomNum(self, atom_num): """ Set the atom number. The basis selector will now allow the user to select a basis set for the specified atom rather than for the entire structure. Note that this function will clear any per-atom basis sets that have been set via `setPerAtom`. :param atom_num: The atom index :type atom_num: int """ self._pop_up.setAtomNum(atom_num)
[docs] def setPerAtom(self, per_atom): """ Set the atom number. The basis selector will now allow the user to select a basis set for the specified atom rather than for the entire structure. Note that this function will clear any per-atom basis sets that have been set via `setPerAtom`. :param atom_num: The atom index :type atom_num: int """ self._pop_up.setPerAtom(per_atom)
[docs] def setBasis(self, basis): """ Set the basis to the requested value. :param basis_full: The requested basis. Note that this name may include `*`'s and `+`'s. :type basis_full: str or NoneType :raise ValueError: If the requested basis was not valid. In these cases, the basis set will not be changed. """ basis = str(basis) if not basis: if self._blank_basis_allowed: self._pop_up.setBasisSafe(basis) else: raise ValueError("Blank basis not allowed") else: # don't use setBasisSafe so that the ValueError will get raised and # the combo boxes won't be changed self._pop_up.setBasis(basis)
[docs] def hasAcceptableInput(self): """ Return True if a basis set has been specified. False otherwise. """ return self._acceptable_basis
[docs] def isValid(self): """ Return True if a basis set has been specified and the basis set applies to the current structure. False otherwise. """ return self.hasAcceptableInput() and self._pop_up.isValid()
class _BasisSelectorPopUp(pop_up_widgets.PopUp): """ The pop up window that is displayed adjacent to `BasisSelectorLineEdit` :param ENABLE_WHEN_NO_STRUC: Should the basis selector be enabled when no structure has been loaded into it? :type ENABLE_WHEN_NO_STRUC: bool """ ENABLE_WHEN_NO_STRUC = True ERROR_FORMAT = "<span style='color: red; font-weight: bold'>%s</span>" # If mmjag isn't already initialized, this decorator speeds up the SPE panel # initialization approximately 3x by removing repeated mmjag_initializations @jag_basis.mmjag_function def setup(self): self._struc = None self._blank_basis_allowed = False self._per_atom = {} self._atom_num = None # Create icons for the basis combo box. Defining the icons as class # variables rather than instance variables results in a "QPixmap: Must # construct a QApplication before a QPaintDevice" error when running # outside of Maestro, so we define them here. self.ps_icon = QtGui.QIcon(":/icons/pseudospectral.png") self.empty_icon = QtGui.QIcon(":/icons/empty.png") self._createWidgets() self._layoutWidgets() self._connectSignals() self._populateBases() def _createWidgets(self): """ Instantiate all basis selector widgets """ self.basis_combo = QtWidgets.QComboBox(self) self.polarization_combo = QtWidgets.QComboBox(self) self.diffuse_combo = QtWidgets.QComboBox(self) self.basis_lbl = QtWidgets.QLabel("Basis set:", self) self.polarization_lbl = QtWidgets.QLabel("Polarization:", self) self.diffuse_lbl = QtWidgets.QLabel("Diffuse:", self) self._createInfoLabels() def _createInfoLabels(self): """ Create self.text_lbl to display text and self.icon_lbl containing an error icon """ self.text_lbl = QtWidgets.QLabel() self.icon_lbl = QtWidgets.QLabel() self.text_lbl.setWordWrap(True) font = self.text_lbl.font() font_metric = QtGui.QFontMetrics(font) font_height = font_metric.height() icon_num = QtWidgets.QStyle.SP_MessageBoxCritical app_style = QtWidgets.QApplication.style() icon = app_style.standardIcon(icon_num) pixmap = icon.pixmap(font_height, font_height) self.icon_lbl.setPixmap(pixmap) self.icon_lbl.setAlignment(Qt.AlignTop | Qt.AlignHCenter) def _layoutWidgets(self): """ Arrange all basis selector widgets """ grid_layout = QtWidgets.QGridLayout(self) grid_layout.addWidget(self.basis_lbl, 0, 0) grid_layout.addWidget(self.basis_combo, 0, 1, 1, 2) grid_layout.addWidget(self.polarization_lbl, 1, 0) grid_layout.addWidget(self.polarization_combo, 1, 1) grid_layout.addWidget(self.diffuse_lbl, 2, 0) grid_layout.addWidget(self.diffuse_combo, 2, 1) info_layout = QtWidgets.QHBoxLayout() info_layout.setContentsMargins(0, 0, 0, 0) info_layout.addWidget(self.icon_lbl) info_layout.addWidget(self.text_lbl) info_layout.addStretch() info_layout.setStretch(1, 1) grid_layout.addLayout(info_layout, 3, 0, 1, 4) def _connectSignals(self): """ Connect the combo box signals """ self.basis_combo.currentIndexChanged.connect(self._update) self.polarization_combo.currentIndexChanged.connect(self._update) self.diffuse_combo.currentIndexChanged.connect(self._update) def _populateBases(self): """ Populate the basis set combo box. """ with suppress_signals(self.basis_combo): for cur_basis in jag_basis.get_bases(): #LA-631G used as a backup for LACVP, do not want to expose #to users. See JAGUAR-9350 if cur_basis.name.lower().startswith('la-631'): continue if cur_basis.is_ps: icon = self.ps_icon else: icon = self.empty_icon self.basis_combo.addItem(icon, cur_basis.name) def setStructure(self, new_struc): """ React to the user changing the selected structure by updating or disabling the basis selection. :param new_struc: The new structure :type new_struc: `schrodinger.structure.Structure` """ self._per_atom = {} if new_struc is not None and new_struc.atom_total: self._struc = new_struc self.basis_combo.setEnabled(True) self.basis_lbl.setEnabled(True) self._update(suppress_signal=True) else: self._struc = None self._disableBasis() def setPerAtom(self, per_atom): """ Set the per-atom basis sets for the current structure. These basis sets will be used when calculating the available basis sets and the number of basis functions. Note that this function will clear any atom number that has been set via `setAtomNum`. :param per_atom: A dictionary of {atom index: basis name} for per-atom basis sets. :type per_atom: dict """ self._per_atom = per_atom self._atom_num = None self._update(suppress_signal=True) def setAtomNum(self, atom_num): """ Set the atom number. The basis selector will now allow the user to select a basis set for the specified atom rather than for the entire structure. Note that this function will clear any per-atom basis sets that have been set via `setPerAtom`. :param atom_num: The atom index :type atom_num: int """ self._atom_num = atom_num self._per_atom = {} self._update(suppress_signal=True) def _disableBasis(self): """ The popup cannot be disabled, so this function is a no op. It is overridden in the BasisSelector subclass. """ def structureUpdated(self): """ React to a change in the structure described by self._struc. i.e. The `schrodinger.structure.Structure` object passed to `setStructure` has not changed, but the structure described by that object has. """ self.setStructure(self._struc) @jag_basis.mmjag_function def _update(self, ignored=None, suppress_signal=False): """ React to a new basis set being selected by updating the labels and the polarization and diffuse combo boxes. :param ignored: This argument is entirely ignored, but is present so Qt callback arguments won't be interpreted as `suppress_signal` :param suppress_signal: If True, the basis_changed signal won't be emitted in response to this update. :type suppress_signal: bool """ self._setBasisAvailabilityAndIcon() basis_name = str(self.basis_combo.currentText()) if basis_name != "": basis = jag_basis.get_basis_by_name(basis_name) self._populatePolarization(basis.nstar) self._populateDiffuse(basis.nplus) self._updateLabels() if not suppress_signal: self._emitBasisChanged() def _updateLabels(self): """ Update the labels in response the newly loaded structure and/or newly selected basis set. """ basis_name = self.getBasis() (sentences, num_funcs) = generate_description(basis_name, self._struc, self._per_atom, self._atom_num) if not num_funcs and sentences[0]: sentences[0] = self.ERROR_FORMAT % sentences[0] text = combine_sentences(sentences) # Make sure that whitespace isn't collapsed. Otherwise, sentence # spacing will be inconsistent between errors and non-errors text = "<span style='white-space:pre-wrap'>%s</span>" % text self.text_lbl.setText(text) if self._struc is None or num_funcs: self.icon_lbl.hide() else: self.icon_lbl.show() # The text label doesn't always calculate its sizeHint correctly until # after it's drawn, so we use a single shot timer to resize the pop up # after it's been drawn. QtCore.QTimer.singleShot(0, self.popUpResized.emit) def _calcNumFunctions(self, basis_name_full): """ Calculate the number of basis functions that will be used for the currently selected structure and basis set :param basis: The full basis name (i.e. including stars and pluses) :type basis_name_full: str :return: A tuple of: - The number of basis functions (int) - Are pseudospectral grids available (bool) If there is no structure set, (0, False) will be returned. :rtype: tuple """ if self._struc is None: return (0, False) else: return num_basis_functions(basis_name_full, self._struc, self._per_atom, self._atom_num) def _populatePolarization(self, nstar): """ Properly populate the polarization combo box. Disable the combo box if there are no options for the curently selected basis set. :param nstar: The maximum number of stars available for the currently selected basis set. :type nstar: int """ self._populatePolarizationOrDiffuse(nstar, "*", self.polarization_combo, self.polarization_lbl) def _populateDiffuse(self, nplus): """ Properly populate the diffuse combo box. Disable the combo box if there are no options for the curently selected basis set. :param nplus: The maximum number of pluses available for the currently selected basis set. :type nplus: int """ self._populatePolarizationOrDiffuse(nplus, "+", self.diffuse_combo, self.diffuse_lbl) def _populatePolarizationOrDiffuse(self, num_symbol, symbol, combo, lbl): """ Properly populate the polarization or diffuse combo boxes. Disable the combo box if there are no options for the currently selected basis set. :param num_symbol: The maximum number of symbols to put into the combo box :type num_symbol: int :param symbol: The type of symbol to put into the combo box :type symbol: str :param combo: The combo box to populate :type combo: `PyQt5.QtWidgets.QComboBox` :param lbl: The label next to the combo box. This label will be disabled and enabled along with the combo box :type lbl: `PyQt5.QtWidgets.QLabel` """ prev_sel = combo.currentIndex() if prev_sel > num_symbol: prev_sel = num_symbol elif prev_sel == -1: prev_sel = 0 opts = ["None", symbol, symbol * 2] with suppress_signals(combo): combo.clear() for cur_opt in opts[:num_symbol + 1]: combo.addItem(cur_opt) combo.setCurrentIndex(prev_sel) enable = num_symbol and (self._struc or self.ENABLE_WHEN_NO_STRUC) lbl.setEnabled(enable) combo.setEnabled(enable) def _addCurrentSuffix(self, basis_name): """ Add the currently selected number of stars and pluses to the given basis name :param basis_name: The basis name without stars or pluses :type basis_name: str :return: The provided basis name with stars and pluses appended :rtype: str """ basis_data = jag_basis.get_basis_by_name(basis_name) polarization = self.polarization_combo.currentIndex() polarization = min(polarization, basis_data.nstar) diffuse = self.diffuse_combo.currentIndex() diffuse = min(diffuse, basis_data.nplus) full_basis = basis_name + polarization * "*" + diffuse * "+" return full_basis def _setBasisAvailabilityAndIcon(self): """ Update which basis sets are enabled in the basis selection combo box. Also update the pseudospectral icons. """ model = self.basis_combo.model() for i in range(model.rowCount()): item = model.item(i) basis_name = str(item.text()) basis_name = self._addCurrentSuffix(basis_name) if self._struc is None: num_funcs = self.ENABLE_WHEN_NO_STRUC is_ps = jag_basis.get_basis_by_name(basis_name).is_ps else: num_funcs, is_ps = self._calcNumFunctions(basis_name) # Set item visibility flag = item.flags() if num_funcs: flag |= Qt.ItemIsEnabled else: flag &= ~Qt.ItemIsEnabled item.setFlags(flag) # Set the pseudospectral icon if is_ps: item.setData(self.ps_icon, Qt.DecorationRole) else: item.setData(self.empty_icon, Qt.DecorationRole) def isValid(self): """ Is a valid basis currently selected? :return: True is a valid basis is currently selected. False otherwise. :rtype: bool """ if self._struc is None: return False basis_name = self.getBasis() num_funcs, is_ps = self._calcNumFunctions(basis_name) return bool(num_funcs) def getBasis(self): """ Get the currently selected basis :return: The currently selected basis :rtype: str """ basis_name = str(self.basis_combo.currentText()) polarization = str(self.polarization_combo.currentText()) diffuse = str(self.diffuse_combo.currentText()) if polarization == "None": polarization = "" if diffuse == "None": diffuse = "" return basis_name + polarization + diffuse def setBasis(self, basis_full=None): r""" Set the basis to the requested value. If no value is given, the default basis (6-31G**) will be used. :param basis_full: The requested basis. Note that this name may include `*`'s and `+`'s. :type basis_full: str or NoneType :raise ValueError: If the requested basis was not valid. In these cases, the basis set will not be changed and basis_changed will not be emitted. """ if basis_full is None: basis_full = jag_basis.default_basis() basis_name, polarization, diffuse = jag_basis.parse_basis(basis_full) index = self.basis_combo.findText(basis_name) if index == -1: raise ValueError("Basis not found") basis_obj = jag_basis.get_basis_by_name(basis_name) if polarization > basis_obj.nstar: err = "%s does not support %i *'s" % (basis_name, polarization) raise ValueError(err) if diffuse > basis_obj.nplus: err = "%s does not support %i +'s" % (basis_name, diffuse) raise ValueError(err) with suppress_signals(self.basis_combo, self.polarization_combo, self.diffuse_combo): self.basis_combo.setCurrentIndex(index) # Call _update() after setting the basis to make sure that the # polarization and diffuse combo boxes are appropriately populated self._update(suppress_signal=True) self.polarization_combo.setCurrentIndex(polarization) self.diffuse_combo.setCurrentIndex(diffuse) self._update(suppress_signal=False) def _emitBasisChanged(self): """ Emit the basis changed signal with the currently selected basis set """ basis_name = self.getBasis() self.dataChanged.emit(basis_name) def setBasisSafe(self, basis_full=""): r""" Set the basis to the requested value. If the provided basis name is invalid, then the combo boxes will be cleared. (With setBasis(), an exception is raised if the basis name is invalid.) :param basis_full: The requested basis set name including any `*`'s and `+`'s. :type basis_full: str :return: True if the basis was valid. False otherwise. :rtype: bool """ try: self.setBasis(basis_full) return True except ValueError: self.basis_combo.setCurrentIndex(-1) self.polarization_combo.setCurrentIndex(-1) self.diffuse_combo.setCurrentIndex(-1) self.polarization_combo.setEnabled(False) self.diffuse_combo.setEnabled(False) if basis_full or not self._blank_basis_allowed: self._showWarning() return False else: self._clearInfoLabels() return True def _clearInfoLabels(self): """ Clear the labels used to present information to the user """ self.text_lbl.setText("") self.icon_lbl.hide() def basisModel(self): """ Return the model of basis set names :return: The model of basis set names :rtype: `PyQt5.QtCore.QAbstractItemModel` """ return self.basis_combo.model() def _showWarning(self): """ Show a warning that the user has specified an invalid basis set """ warning = "Invalid basis set" text = self.ERROR_FORMAT % warning self.text_lbl.setText(text) self.icon_lbl.show() QtCore.QTimer.singleShot(0, self.popUpResized.emit) def setBlankBasisAllowed(self, allowed): """ Specify whether a blank basis set is allowed or should result in an "Invalid basis set" warning. :param allowed: True if a blank basis set should be allowed. False if a blank basis set should result in an invalid basis set warning. :type allowed: bool """ self._blank_basis_allowed = allowed if not self.getBasis(): # If the basis is currently blank, reset the labels self.setBasisSafe() def estimateMaxHeight(self): """ Estimate the maximum height of this widget (since the popup's height may increase when more text is added to the bottom label). :return: The estimated maximum height :rtype: int :note: This function is only an estimate since Qt won't accurately calculate height until after a redraw. As such, the maximum label height may not be accurate and the presence/absence of the warning icon isn't taken into account. """ cur_height = self.sizeHint().height() cur_text = self.text_lbl.text() cur_lbl_height = self.text_lbl.sizeHint().height() height_without_lbl = cur_height - cur_lbl_height # Use a sample label text to estimate the maximum label height long_text = ( "1610 basis functions. Pseudospectral grids not " "available. Effective core potentials on heavy atoms. Non-ECP " "atoms use CC-PVQZ(-G) basis.") self.text_lbl.setText(long_text) # self.sizeHint() won't be updated until after a redraw, so manually # calculate the pop up height with the new label text. max_lbl_height = self.text_lbl.sizeHint().height() self.text_lbl.setText(cur_text) max_height = height_without_lbl + max_lbl_height return max_height
[docs]class UpperCaseValidator(QtGui.QValidator): """ A QValidator that converts all input to uppercase """
[docs] def validate(self, text, pos): """ See PyQt documentation for argument and return value documentation """ return self.Acceptable, text.upper(), pos
[docs]class BasisSelector(_BasisSelectorPopUp): """ A widget for selecting a Jaguar basis set without a line edit. BasisSelectorLineEdit should be favored over this class for new panels. This widget may be directly connected to a `schrodinger.ui.qt.input_selector.InputSelector` instance by passing it to the initializer or to `setInputSelector`. Alternatively, this widget may be used without an input selector by passing new structures to `structureChanged`. """ ENABLE_WHEN_NO_STRUC = False
[docs] def __init__(self, parent=None, input_selector=None): super(BasisSelector, self).__init__(parent) self._setDefault() self.setFrameShape(self.NoFrame) self.setAutoFillBackground(False) self.basis_changed = self.dataChanged # for backwards compatibility self.input_selector = None if input_selector is not None: self.setInputSelector(input_selector) else: # Make sure that the selector is disabled if there is no structure self.structureChanged(None)
def _createInfoLabels(self): """ Create the labels used to display information to the user """ self.line1_lbl = QtWidgets.QLabel(self) self.line2_lbl = QtWidgets.QLabel(self) def _updateLabels(self): """ Update the labels in response the newly loaded structure and/or newly selected basis set. """ basis_name = self.getBasis() (sentences, num_funcs) = generate_description(basis_name, self._struc) line1_text = combine_sentences(sentences[0:2]) line2_text = combine_sentences(sentences[2:4]) self.line1_lbl.setText(line1_text) self.line2_lbl.setText(line2_text) def _layoutWidgets(self): """ Arrange all basis selector widgets """ hlayout = QtWidgets.QHBoxLayout() vlayout = QtWidgets.QVBoxLayout(self) vlayout.setContentsMargins(0, 0, 0, 0) hlayout.addWidget(self.basis_lbl) hlayout.addWidget(self.basis_combo) hlayout.addWidget(self.polarization_lbl) hlayout.addWidget(self.polarization_combo) hlayout.addWidget(self.diffuse_lbl) hlayout.addWidget(self.diffuse_combo) hlayout.addStretch() vlayout.addLayout(hlayout) vlayout.addWidget(self.line1_lbl) vlayout.addWidget(self.line2_lbl)
[docs] def setInputSelector(self, input_selector): """ Set the input selector that this widget should respond to :param input_selector: The AppFramework input selector frame :type input_selector: `schrodinger.ui.qt.input_selector.InputSelector` """ self.input_selector = input_selector self.input_selector.input_changed.connect( self._inputSelectorStructureChanged) self._inputSelectorStructureChanged()
def _setDefault(self): """ Set the basis, polarization, and diffuse settings using the Jaguar defaults. """ default_basis = jag_basis.default_basis() basis, polarization, diffuse = jag_basis.parse_basis(default_basis) basis_index = self.basis_combo.findText(basis) with suppress_signals(self.basis_combo, self.polarization_combo, self.diffuse_combo): self.basis_combo.setCurrentIndex(basis_index) self._update(suppress_signal=True) self.polarization_combo.setCurrentIndex(polarization) self.diffuse_combo.setCurrentIndex(diffuse) def _inputSelectorStructureChanged(self): """ React to the user changing the input selector structure. Note that only the first input selector structure will be used. All others will be ignored. """ try: struc = next(self.input_selector.structures(False)) except (IOError, StopIteration): struc = None self.structureChanged(struc)
[docs] def structureChanged(self, new_struc): """ React to the user changing the selected structure by updating or disabling the basis selection. :param new_struc: The new structure :type new_struc: `schrodinger.structure.Structure` """ self.setStructure(new_struc)
def _disableBasis(self): """ Disable all basis selection combo boxes. """ for cur_widget in (self.basis_lbl, self.basis_combo, self.polarization_combo, self.polarization_lbl, self.diffuse_lbl, self.diffuse_combo): cur_widget.setEnabled(False) self.line1_lbl.setText("0 basis functions.") self.line2_lbl.setText("")
[docs]class BasisSetListWidgetItem(QtWidgets.QListWidgetItem): """ Custom QListWidgetItem that is associated with a specific basis set. """
[docs] def __init__(self, text, full_name=None, is_diffuse=False, is_pseudospectral=False, is_ecp=False, is_relativistic=False, is_rimp2_compatible=False): """ :param text: Text for this list item. :type text: str :param full_name: Full name for the basis set. If not specified, the value of text will be used. :type full_name: str or None :param is_diffuse: Whether this basis set is diffuse. :type is_diffuse: bool :param is_pseudospectral: Whether this basis set is pseudospectral. :type is_pseudospectral: bool :param is_ecp: Whether this basis set is ECP. :type is_ecp: bool :param is_relativistic: Whether this basis set is relativistic. :type is_relativistic: bool :param is_rimp2_compatible: Whether this basis set has an automatically associated rimp2_auxbas :type is_rimp2_compatible: bool """ super().__init__(text) self.full_name = full_name or text self.is_diffuse = is_diffuse self.is_pseudospectral = is_pseudospectral self.is_ecp = is_ecp self.is_relativistic = is_relativistic self.is_rimp2_compatible = is_rimp2_compatible
[docs]class BasisSelectorFilterPopUp(filter_list.FilterListPopUp): """ A pop up widget that allows for dynamic filtering and selection of basis sets. """
[docs] def __init__(self, parent): """ :param parent: The parent widget. :type parent: QtWidgets.QWidget """ self._struc = None self._allow_no_struc = True self._blank_basis_allowed = False self._per_atom = {} self._atom_num = None diffuse_func = lambda li: li.is_diffuse diffuse_cb = filter_list.FilterCheckBox("Diffuse functions", diffuse_func) ps_func = lambda li: li.is_pseudospectral ps_cb = filter_list.FilterCheckBox("Pseudospectral", ps_func) ecp_func = lambda li: li.is_ecp ecp_cb = filter_list.FilterCheckBox("ECP", ecp_func) relativistic_func = lambda li: li.is_relativistic relativistic_cb = filter_list.FilterCheckBox("Relativistic", relativistic_func) rimp2_func = lambda li: li.is_rimp2_compatible rimp2_cb = filter_list.FilterCheckBox("RI-MP2-compatible", rimp2_func) cbs = (diffuse_cb, ps_cb, ecp_cb, relativistic_cb, rimp2_cb) list_items = get_basis_set_list_items() super().__init__(parent, list_items, cbs, 'Limit list to matching basis sets:', 'Basis sets must match all checked categories.', 'No basis sets match all checked categories.')
[docs] def setStructure(self, new_struc): """ React to the user changing the selected structure by updating or disabling the basis selection. :param new_struc: The new structure or None to clear structure. :type new_struc: schrodinger.structure.Structure or None """ self._per_atom = {} if new_struc and new_struc.atom_total: self._struc = new_struc self._list_widget.setEnabled(True) self._update(suppress_signal=True) else: self._struc = None
[docs] def setPerAtom(self, per_atom): """ Set the per-atom basis sets for the current structure. These basis sets will be used when calculating the available basis sets and the number of basis functions. Note that this function will clear any atom number that has been set via `setAtomNum`. :param per_atom: A dictionary of {atom index: basis name} for per-atom basis sets. :type per_atom: dict """ self._per_atom = per_atom self._atom_num = None self._update(suppress_signal=True)
[docs] def setAtomNum(self, atom_num): """ Set the atom number. The basis selector will now allow the user to select a basis set for the specified atom rather than for the entire structure. Note that this function will clear any per-atom basis sets that have been set via `setPerAtom`. :param atom_num: The atom index :type atom_num: int """ self._atom_num = atom_num self._per_atom = {} self._update(suppress_signal=True)
[docs] def structureUpdated(self): """ React to a change in the structure described by self._struc. i.e. The `schrodinger.structure.Structure` object passed to `setStructure` has not changed, but the structure described by that object has. """ self.setStructure(self._struc)
@jag_basis.mmjag_function def _update(self, *, suppress_signal=False): """ React to a new basis set being selected. :param suppress_signal: If True, the basis_changed signal won't be emitted in response to this update. :type suppress_signal: bool """ self._setBasisAvailabilityAndIcon() basis_item = self._list_widget.currentItem() if not suppress_signal: self._emitBasisChanged() def _calcNumFunctions(self, basis_name_full): """ Calculate the number of basis functions that will be used for the currently selected structure and basis set :param basis_name_full: The full basis name (i.e. including stars and pluses) :type basis_name_full: str :return: A tuple of: - The number of basis functions (int) - Are pseudospectral grids available (bool) If there is no structure set, (0, False) will be returned. :rtype: tuple """ if self._struc is None: return (0, False) else: return num_basis_functions(basis_name_full, self._struc, self._per_atom, self._atom_num) def _setBasisAvailabilityAndIcon(self): """ Update which basis sets are enabled in the basis selection list. """ for i in range(self._list_widget.count()): item = self._list_widget.item(i) basis_name = item.full_name if self._struc is None: num_funcs = self._allow_no_struc else: num_funcs, is_ps = self._calcNumFunctions(basis_name) # Set item visibility flag = item.flags() if num_funcs: flag |= Qt.ItemIsEnabled else: flag &= ~Qt.ItemIsEnabled item.setFlags(flag)
[docs] def isValid(self): """ Is a valid basis currently selected? :return: True is a valid basis is currently selected. False otherwise. :rtype: bool """ if self._struc is None: return self._allow_no_struc basis_name = self.getBasis() num_funcs, is_ps = self._calcNumFunctions(basis_name) return bool(num_funcs)
[docs] def getBasis(self): """ Get the currently selected basis :return: The currently selected basis or None if no basis is selected. :rtype: str or None """ basis_item = self._list_widget.currentItem() if not basis_item: return None return basis_item.full_name
[docs] def setBasis(self, basis_full=None): r""" Set the basis to the requested value. If no value is given, the default basis (6-31G**) will be used. :param basis_full: The requested basis. Note that this name may include `*`'s and `+`'s. If None is passed the default will be used. :type basis_full: str or NoneType :raise ValueError: If the requested basis was not valid. In these cases, the basis set will not be changed and basis_changed will not be emitted. """ if basis_full is None: basis_full = jag_basis.default_basis() basis_name, polarization, diffuse = jag_basis.parse_basis(basis_full) basis_full = get_basis_display_name(basis_full) items = self._list_widget.findItems(basis_full, Qt.MatchFlag.MatchFixedString) if len(items) != 1: raise ValueError(f"Basis not found: {basis_full}") self._list_widget.setCurrentItem(items[0]) self._update(suppress_signal=False)
def _emitBasisChanged(self): """ Emit the basis changed signal with the currently selected basis set """ basis_name = self.getBasis() self.dataChanged.emit(basis_name)
[docs] def setBasisSafe(self, basis_full=""): r""" Set the basis to the requested value. If the provided basis name is invalid, then the combo boxes will be cleared. (With setBasis(), an exception is raised if the basis name is invalid.) :param basis_full: The requested basis set name including any `*`'s and `+`'s. :type basis_full: str :return: True if the basis was valid. False otherwise. :rtype: bool """ try: self.setBasis(basis_full) return True except ValueError: self._list_widget.setCurrentRow(-1) if basis_full or not self._blank_basis_allowed: return False else: return True
[docs] def basisModel(self): """ Return the model of basis set names :return: The model of basis set names :rtype: PyQt5.QtCore.QAbstractItemModel """ return self._list_widget.model()
[docs] def setNoStrucAllowed(self, allow_no_struc): """ Set whether a structure must be set on this popup. :param allow_no_struc: Whether a structure must be set or not. :type allow_no_struc: bool """ self._allow_no_struc = allow_no_struc
[docs] def setBlankBasisAllowed(self, allowed): """ Specify whether a blank basis set is allowed :param allowed: True if a blank basis set should be allowed :type allowed: bool """ self._blank_basis_allowed = allowed if not self.getBasis(): # If the basis is currently blank, reset the labels self.setBasisSafe()
[docs] def hasAcceptableInput(self): """ Return True if the current basis set is valid, False otherwise. :rtype: bool """ basis = self.getBasis() if not basis: return self._blank_basis_allowed return self.isValid()
[docs]class BasisSelectorFilterListToolButton( filter_list.ToolButtonWithFilterListPopUp): """ Custom tool button with a basis selector filter list pop up. """ POP_UP_CLASS = BasisSelectorFilterPopUp
[docs] def getBasis(self): return self._pop_up.getBasis()
[docs] def setBasis(self, basis): self._pop_up.setBasis(basis)
[docs] def hasAcceptableInput(self): """ Return True if a valid basis set has been specified. False otherwise. """ return self._pop_up.hasAcceptableInput()
[docs] def setStructure(self, struct): """ Set the structure on the popup. :param struct: Structure to be set, or None to unset a structure. :type struct: schrodinger.structure.Structure or None """ self._pop_up.setStructure(struct)
[docs] def applySettings(self, settings): """ Apply the specified filter settings to the pop up :param settings: Settings to be applied :type settings: dict """ self._pop_up.applySettings(settings)
[docs]class FilterBasisSelectorReadOnlyLineEdit(pop_up_widgets.LineEditWithPopUp): """ A read-only line edit used as an editor for table models with a BasisSelectorFilterPopUp. :ivar filtersChanged: Signal emitted when filters are toggled. emits a dict of current filter settings. :type filtersChanged: QtCore.pyQtSignal(dict) """ filtersChanged = QtCore.pyqtSignal(dict)
[docs] def __init__(self, parent): super().__init__(parent, BasisSelectorFilterPopUp) self._pop_up.filtersChanged.connect(self.filtersChanged) self._pop_up.setNoStrucAllowed(False) self._pop_up.setBlankBasisAllowed(True) self.setReadOnly(True) self.setFocusPolicy(Qt.StrongFocus)
[docs] def setStructure(self, struc): """ Set the structure to use for determining basis set information and availability :param struc: The structure to use :type struc: `schrodinger.structure.Structure` """ self._pop_up.setStructure(struc)
[docs] def setAtomNum(self, atom_num): """ Set the atom number. The basis selector will now allow the user to select a basis set for the specified atom rather than for the entire structure. Note that this function will clear any per-atom basis sets that have been set via `setPerAtom`. :param atom_num: The atom index :type atom_num: int """ self._pop_up.setAtomNum(atom_num)
[docs] def setPerAtom(self, per_atom): """ Set the atom number. The basis selector will now allow the user to select a basis set for the specified atom rather than for the entire structure. Note that this function will clear any per-atom basis sets that have been set via `setPerAtom`. :param atom_num: The atom index :type atom_num: int """ self._pop_up.setPerAtom(per_atom)
[docs] def setBasis(self, basis_full=None): r""" Set the basis to the requested value. :param basis_full: The requested basis. Note that this name may include `*`'s and `+`'s. :type basis_full: str or NoneType :raise ValueError: If the requested basis was not valid. In these cases, the basis set will not be changed. """ self._pop_up.setBasis(basis_full)
[docs] def popUpUpdated(self): """ Update the text edit's basis based on the value in the pop up. """ basis = self._pop_up.getBasis() if basis is not None: self.setText(basis)
[docs] def applySettings(self, settings): """ Apply the specified filter settings to the pop up. :param settings: Filter settings to apply :type settings: dict """ self._pop_up.applySettings(settings)