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