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 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 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)
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 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 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)