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

import enum
import sys
from collections import defaultdict
from collections import namedtuple

import schrodinger
from schrodinger import project
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 delegates as qt_delegates
from schrodinger.ui.qt import input_selector
from schrodinger.ui.qt import pop_up_widgets
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import table_helper
# Flags for including/removing an entry from the workspace
from schrodinger.ui.qt.delegates import NUM_INCLUDED_ENTRIES_ROLE
from schrodinger.ui.qt.delegates import WS_INCLUDE
from schrodinger.ui.qt.delegates import WS_INCLUDE_ONLY
from schrodinger.ui.qt.delegates import WS_REMOVE

from . import basis_selector
from . import theory_selector
from . import utils as gui_utils
from .utils import MOLECULAR_CHARGE_PROP
from .utils import SPIN_MULT_PROP
from .utils import THEORY_DFT
from .utils import THEORY_HF
from .utils import THEORY_LMP2
from .utils import THEORY_RIMP2

maestro = schrodinger.get_maestro()

# Custom roles for InputEntriesModel
(STRUCTURE_ROLE, DEFAULT_ROLE, SORT_ROLE,
 PER_ATOM_BASIS_ROLE) = list(range(Qt.UserRole, Qt.UserRole + 4))

UseFrom = enum.Enum("UseFrom", ("included", "selected"))

# A translation between standard InputSelector stats and the the UseFrom enum
SELECTOR_TO_USEFROM = {
    input_selector.InputSelector.SELECTED_ENTRIES: UseFrom.selected,
    input_selector.InputSelector.INCLUDED_ENTRIES: UseFrom.included
}

DEFAULT_DFTNAME = 'B3LYP-D3'

_Basis = namedtuple("Basis", ("basis", "polarization", "diffuse"))


[docs]class Basis(_Basis): """ Class for representing a basis set. The basis set name, polarization (i.e. `*`'s), and diffuse (i.e. `+`'s) are stored separately. """ def __str__(self): basis_full = self.basis + self.diffuse * "+" + self.polarization * "*" return basis_full
[docs] @classmethod def fromText(cls, text): r""" Create a Basis object from a string :param text: The full basis set name including `*`'s and `+`'s :type text: str """ basis = jag_basis.parse_basis(text) return cls(*basis)
def __bool__(self): """ This object evaluates as truthy if is non-empty """ return bool(self.basis)
[docs]class ProjEntry(object): """ A class for accessing data about a project table row and the associated structure """
[docs] def __init__(self, row=None): """ Instantiate a new object :param row: The project row. If not provided, no data will be loaded. :type row: `schrodinger.project.ProjectRow` or NoneType """ self.included = False self.entry_id = None self.title = "" self.charge = None self.spin_mult = None self._row_charge = 0 self.row_spin_mult = None self._num_protons = None self.basis = None self.theory = None self._row = None self._up_to_date = False # self._up_to_date will be True if self._row_charge and # self._num_protons have been recalculated since the last project table # update. It will be False otherwise. if row is not None: self.update(row)
# Note: The charge and spin_mult attributes store the values that the # user has entered into the input table. The row_charge and # row_spin_mult attributes store the values taken from the project table # row or the structure itself. The values in charge and spin_mult take # "priority" over row_charge and row_spin_mult. If the values in charge # and/or spin_mult are None or invalid, then the corresponding row_* # value will be used instead.
[docs] def update(self, row): """ Update this entry with information from the provided row :param row: The project row :type row: `schrodinger.project.ProjectRow` """ self._row = row self.included = (row.in_workspace != project.NOT_IN_WORKSPACE) self.title = row.title self.entry_id = row.entry_id self.row_spin_mult = row[SPIN_MULT_PROP] self._up_to_date = False
def _delayedChargeCalc(self): """ Calculate the row charge and the number of protons if the values are out of date. These calculations are delayed until necessary to avoid slowing down project table updates. """ if self._up_to_date: return self._updateRowCharge() self._calcNumProtons() self._up_to_date = True def _updateRowCharge(self): """ Update the molecular charge from the project table row. This value will be used if there is no user-specified charge in the table. """ mol_charge_prop = self._row[MOLECULAR_CHARGE_PROP] if mol_charge_prop is not None: self._row_charge = mol_charge_prop else: self._row_charge = self._calcCharge()
[docs] def reset(self): """ Reset any user-specified settings """ self.charge = None self.spin_mult = None self.basis = None self.theory = None
[docs] def setIncluded(self, value): """ Include or remove this entry from the workspace. :param value: A flag indicated whether the entry should be removed from the workspace (WS_REMOVE), included in the workspace(WS_INCLUDED), or set as the only entry in the workspace (WS_INCLUDE_ONLY) :type value: int """ if value == WS_REMOVE: self._row.in_workspace = project.NOT_IN_WORKSPACE elif value == WS_INCLUDE: self._row.in_workspace = project.IN_WORKSPACE elif value == WS_INCLUDE_ONLY: self._row.includeOnly() self.included = (value != WS_REMOVE)
def _calcNumProtons(self): """ Calculate the number of protons in the current structure. The result will be stored in self._num_protons. """ struc = self._row.getStructure(copy=False) self._num_protons = gui_utils.calculate_num_protons(struc) def _calcCharge(self): """ Calculate the formal charge on the molecule by summing the charge on each atom :return: The formal charge of the molecule :rtype: int """ struc = self._row.getStructure(copy=False) charge = sum([atom.formal_charge for atom in struc.atom]) return charge
[docs] def getCharge(self): """ Get the user-specified charge on the structure. If no charge has been set, the default charge will be returned. :return: A tuple of: - The charge (int) - Is this a user-specified charge (True) or the default charge (False) :rtype: tuple """ self._delayedChargeCalc() if self.charge is not None: return (self.charge, True) else: return (self._row_charge, False)
def _getSpinMult(self, force_default): """ Get the user-specified spin multiplicity on the structure. If no spin multiplicity has been set or if the user-specified multiplicity is incompatible with the current charge, then the default spin multiplicity will be returned. :param force_default: If True, the default spin multiplicity will always be returned even if there is a user-specified spin multiplicity :type force_default: bool :return: A tuple of: - The spin multiplicity (int) - Is this a user-specified spin multiplicity (True) or the default spin multiplicity (False) :rtype: tuple """ self._delayedChargeCalc() (charge, user_set_charge) = self.getCharge() num_electrons = self._num_protons - charge if (not force_default and self.spin_mult and ((self.spin_mult + num_electrons) % 2)): return (self.spin_mult, True) elif (self.row_spin_mult is not None and (self.row_spin_mult + num_electrons) % 2): return (self.row_spin_mult, False) else: default_spin_mult = num_electrons % 2 + 1 return (default_spin_mult, False)
[docs] def getSpinMult(self): """ Get the user-specified spin multiplicity on the structure. If no spin multiplicity has been set or if the user-specified multiplicity is incompatible with the current charge, then the default spin multiplicity will be returned. :return: A tuple of: - The spin multiplicity (int) - Is this a user-specified spin multiplicity (True) or the default spin multiplicity (False) :rtype: tuple """ return self._getSpinMult(False)
[docs] def getDefaultSpinMult(self): """ Get the default spin multiplicity on the structure for the current charge :return: The default spin multiplicity :rtype: int """ (default, user_set) = self._getSpinMult(True) return default
[docs] def getStructure(self): """ Get the entry structure :return: The structure object :rtype: `schrodinger.structure.Structure` """ return self._row.getStructure()
[docs] def setBasisFromText(self, basis): """ Set the basis :param basis: The full basis set name including `*`'s and `+`'s :type basis: str """ if not basis: self.basis = None else: self.basis = Basis.fromText(basis)
[docs] def setMethodFromText(self, theory): """ Set the method :param theory: The method to be set. :type theory: str """ self.theory = theory or None
[docs] def getIgnoredSpinMult(self): """ If the user has set an invalid spin multiplicity, return it :return: If the user has set a spin multiplicity but it's being ignored, return the user-specified spin multiplicity. Otherwise, return None. :rtype: int or NoneType """ if self.spin_mult is not None: (spin_mult, user_set) = self.getSpinMult() if not user_set: return self.spin_mult
[docs] def setSpinMult(self, spin_mult): """ Set the spin multiplicity :param spin_mult: The spin multiplicity to set :type spin_mult: int :return: True if this is a valid spin multiplicity for the current charge. False otherwise. :rtype: bool """ self.spin_mult = spin_mult return self.getIgnoredSpinMult() is None
[docs] def getStrucChargeAndSpinMult(self): """ Get the structure, charge, and spin multiplicity settings. (This is intended for use in loading data into a ProjEntryTuple object.) :return: A tuple of (entry_id, structure, charge, spin multiplicity) :rtype: tuple """ struc = self.getStructure() (charge, user_set_charge) = self.getCharge() (spin_mult, user_set_sm) = self.getSpinMult() return (self.entry_id, struc, charge, spin_mult)
@property def row_charge(self): """ Make sure we provide an up to date row charge value """ self._delayedChargeCalc() return self._row_charge
ProjEntryTuple = namedtuple( "ProjEntryTuple", ("entry_id", "struc", "charge", "spin_mult", "basis", "theory")) """ A simplified class for storing data about a project table row and the associated structure. Unlike ProjEntry, ProjEntryTuple contains data about the basis set even for structures using the default basis. """
[docs]class InputEntriesColumns(object): """ Column constants for the selected entries table """ HEADERS = [ "ID", "In", "Entry Title", "Charge", "Spin Mult.", 'Theory', "Basis Set" ] NUM_COLS = len(HEADERS) ID, INCLUSION, TITLE, CHARGE, SPIN_MULT, THEORY, BASIS = list( range(NUM_COLS))
[docs]class InputEntriesModel(QtCore.QAbstractTableModel): """ The data model for the input entries table :cvar ERROR_BACKGROUND_BRUSH: The brush used to paint the background of cells containing invalid data :vartype ERROR_BACKGROUND_BRUSH: `PyQt5.QtGui.QBrush` :ivar show_tool_tip: A signal indicating that the tool tip for the specified cell should explicitly be shown. This is used to notify the user when an invalid spin multiplicity has been entered. (Note that this signal has nothing to do with tool tips being shown when the user hovers over a cell.) This signal is emitted with the index of the cell to display the tool tip for. :vartype show_tool_tip: `PyQt5.QtCore.pyqtSignal` :ivar basisChanged: Signal emitted when a new basis set is selected. :vartype basisChanged: QtCore.pyqtSignal :ivar theoryChanged: Signal emitted when a new theory level is selected. :vartype theoryChanged: QtCore.pyQtSignal """ COLUMN = InputEntriesColumns ERROR_BACKGROUND_BRUSH = gui_utils.ERROR_BACKGROUND_BRUSH show_tool_tip = QtCore.pyqtSignal(QtCore.QModelIndex) basisChanged = QtCore.pyqtSignal() theoryChanged = QtCore.pyqtSignal() ERROR_PRE = "<span style='color:red'>" ERROR_POST = "</span>" ROW_CLASS = ProjEntry
[docs] def __init__(self, parent): super(InputEntriesModel, self).__init__(parent) self.all_rows = defaultdict(self.ROW_CLASS) self.cur_table_rows = [] self._num_included_rows = 0 default_basis_name = jag_basis.default_basis() default_basis = jag_basis.parse_basis(default_basis_name) self.default_basis = Basis(*default_basis) self.default_theory = DEFAULT_DFTNAME self._per_atom_basis_model = None # default to included rows rather than selected rows self._source = UseFrom.included self._row_func = lambda proj: proj.included_rows self.valid_rimp2_basis_sets = self._getCompatibleRIMP2BasisSets()
def _getCompatibleRIMP2BasisSets(self): """ Returns list of basis set names, which are compatible with RI-MP2 theory. :return: list of basis set names :rtype: List[str] """ basis_set_items = basis_selector.get_basis_set_list_items() valid_basis_sets = [ basis_set.full_name for basis_set in basis_set_items if basis_set.is_rimp2_compatible ] return valid_basis_sets
[docs] def setPerAtomBasisModel(self, per_atom_basis_model): """ Connect this model to the per-atom basis set model from the Basis Set sub-tab. :param per_atom_basis_model: The per-atom basis set model :type per_atom_basis_model: `schrodinger.application.jaguar.gui.tabs. sub_tab_widgets.basis_set_widgets.BasisSetModel` """ self._per_atom_basis_model = per_atom_basis_model per_atom_basis_model.basisChanged.connect(self.perAtomBasisChanged)
[docs] def perAtomBasisChanged(self, eid): """ Respond to the user changing a per-atom basis set for the specified entry ID. A per-atom basis set change could change the validity of the current basis set for the structure, so we emit dataChanged. This triggers the view to re-color the background of the cell. :param eid: The entry ID :type eid: str """ proj_row = self.all_rows[eid] # If an entry ID isn't included in cur_table_rows, then it shouldn't be # included in the basis sub-tab table. As such, we should never get a # ValueError from the .index() call. row_num = self.cur_table_rows.index(proj_row) index = self.index(row_num, self.COLUMN.BASIS) self.dataChanged.emit(index, index)
[docs] def projectUpdated(self): """ Update the table when the project is updated """ try: proj = maestro.project_table_get() except project.ProjectException: self.clearRows() return self._updateProjData(proj) self._updateCurTableRows(proj) self.basisChanged.emit()
[docs] def workspaceChanged(self, what_changed): """ If the workspace changed, update data in case the user changed the charge of a molecule. :param what_changed: A flag indicating what changed in the workspace :type what_changed: str :note: This function is called before the workspace changes have propagated to the project table. As such, we use a QTimer to wait until after the changes have propagated before updating. """ if what_changed in (maestro.WORKSPACE_CHANGED_EVERYTHING, maestro.WORKSPACE_CHANGED_PROPERTIES, maestro.WORKSPACE_CHANGED_CONNECTIVITY): QtCore.QTimer.singleShot(0, self.projectUpdated)
def _updateCurTableRows(self, proj): """ Update self.cur_table_rows so it reflects the currently selected or included project rows and inform the view of the changes. :param proj: The current maestro project :type proj: `schrodinger.project.Project` :note: At the end of this function, self.cur_table_rows will be equal to [self.all_rows[row.entry_id] for row in proj.selected_rows] (or proj.included_rows). This function updates self.cur_table_rows such that the view is aware of which individual rows are being added and deleted by using beginInsertRow()/endInsertRow() and beginRemoveRow()/endRemoveRow(). This allows the view to properly update its current selection, current selected index, etc. If this function were implemented using beginResetModel()/endResetModel(), the table view would lose its selection and scroll back to the upper-left corner every time the user included/selected a new structure. """ new_sel_rows = list(self._row_func(proj)) new_sel_rows.sort(key=lambda row: int(row.entry_id)) # Go through new_sel_rows and self.cur_table_rows and update # self.cur_table_rows one row at a time row_index = 0 parent_index = QtCore.QModelIndex() while new_sel_rows: new_row = new_sel_rows[0] new_eid = new_row.entry_id try: cur_eid = self.cur_table_rows[row_index].entry_id except IndexError: # If we've already gone through all of the rows in the table, # use an "infinite" value for cur_eid cur_eid = sys.maxsize + 1 if int(new_eid) < int(cur_eid): # There's an entry id in new_sel_rows that's not in # self.cur_table_rows, so insert it del new_sel_rows[0] self.beginInsertRows(parent_index, row_index, row_index) self.cur_table_rows.insert(row_index, self.all_rows[new_eid]) self.endInsertRows() row_index += 1 elif int(new_eid) == int(cur_eid): # The two entry ids are the same, so keep it del new_sel_rows[0] row_index += 1 elif int(new_eid) > int(cur_eid): # There's an entry id in self.cur_table_rows, that's not in # new_sel_rows, so delete it self.beginRemoveRows(parent_index, row_index, row_index) del self.cur_table_rows[row_index] self.endRemoveRows() # If we've gone through all of the newly selected rows and there are # still rows left at the end of self.cur_table_rows, delete them all. last_row_index = len(self.cur_table_rows) - 1 if row_index <= last_row_index: self.beginRemoveRows(parent_index, row_index, last_row_index) del self.cur_table_rows[row_index:] self.endRemoveRows() def _updateProjData(self, proj): """ Update the data in self.all_rows for all selected rows and inform the view of the changes. :param proj: The current maestro project :type proj: `schrodinger.project.Project` """ for proj_row in self._row_func(proj): eid = proj_row.entry_id self.all_rows[eid].update(proj_row) self._num_included_rows = len(proj.included_rows) upper_left = self.index(0, 0) lower_right = self.index(self.rowCount(), self.columnCount()) self.dataChanged.emit(upper_left, lower_right)
[docs] def clearRows(self): """ Clear all row data """ self.beginResetModel() self.cur_table_rows = [] self._num_included_rows = 0 self.all_rows.clear() self.endResetModel()
[docs] def rowCount(self, parent=None): return len(self.cur_table_rows)
[docs] def columnCount(self, parent=None): return self.COLUMN.NUM_COLS
[docs] def data(self, index, role=Qt.DisplayRole): table_row = index.row() col = index.column() proj_row = self.cur_table_rows[table_row] if role in (Qt.DisplayRole, SORT_ROLE): return self._displayAndSortData(col, proj_row, role) elif role in (Qt.FontRole, Qt.ForegroundRole): return self._formattingData(col, proj_row, role) elif role == Qt.ToolTipRole: return self._toolTipData(col, proj_row, role) else: return self._otherData(col, proj_row, role)
[docs] def copyChargeMultBasisFromModel(self, copy_model): """ Copy the charge, spin and basis set information from the given model to this model. :type copy_model: `InputEntriesModel` :param copy_model: The model to copy information from :raise RuntimeError: If the rows in the model are not in the same order """ # This method is used in the Jaguar Multistage panel when copying one # stage into another for rindex, copy_proj_row in enumerate(copy_model.cur_table_rows): if copy_proj_row.entry_id != self.cur_table_rows[rindex].entry_id: raise RuntimeError('The Input Tab to copy is not in the same ' 'order as the current Input Tab') for getter, col in [('getCharge', self.COLUMN.CHARGE), ('getSpinMult', self.COLUMN.SPIN_MULT)]: value, user_set = getattr(copy_proj_row, getter)() if user_set: my_index = self.createIndex(rindex, col) self.setData(my_index, value) basis = copy_proj_row.basis if basis: my_index = self.createIndex(rindex, self.COLUMN.BASIS) self.setData(my_index, str(basis))
def _displayAndSortData(self, col, proj_row, role): """ Return data for the display or sort role :param col: The column to display data for :type col: int :param proj_row: The project row object to display data for :type proj_row: ProjEntry :param role: The role to display data for. Must be Qt.DisplayRole or SORT_ROLE. :type role: int """ if col == self.COLUMN.ID: if role == Qt.DisplayRole: return proj_row.entry_id else: # Entry IDs should be sorted numerically return int(proj_row.entry_id) if col == self.COLUMN.INCLUSION: return proj_row.included elif col == self.COLUMN.TITLE: return proj_row.title elif col == self.COLUMN.CHARGE: charge, user_set = proj_row.getCharge() return charge elif col == self.COLUMN.SPIN_MULT: spin_mult, user_set = proj_row.getSpinMult() return spin_mult elif col == self.COLUMN.BASIS: basis = self._getDisplayBasis(proj_row) if role == Qt.DisplayRole: return str(basis) else: return basis elif col == self.COLUMN.THEORY: return proj_row.theory or self.default_theory def _formattingData(self, col, proj_row, role): """ Return data for the font or foreground role :param col: The column to display data for :type col: int :param proj_row: The project row object to display data for :type proj_row: ProjEntry :param role: The role to display data for. Must be Qt.FontRole or Qt.ForegroundRole. :type role: int """ if col == self.COLUMN.CHARGE: charge, user_set = proj_row.getCharge() elif col == self.COLUMN.SPIN_MULT: spin_mult, user_set = proj_row.getSpinMult() elif col == self.COLUMN.BASIS: basis = proj_row.basis per_atom = self._getPerAtomBasis(proj_row) user_set = basis or per_atom elif col == self.COLUMN.THEORY: user_set = proj_row.theory is not None else: user_set = True if not user_set: if role == Qt.FontRole: font = QtGui.QFont() font.setItalic(True) return font elif role == Qt.ForegroundRole: if (col != self.COLUMN.BASIS or self._hasValidBasis(proj_row)): return QtGui.QBrush(Qt.lightGray) # Gray is unreadable against the red background, so # don't change the font color for invalid basis sets def _toolTipData(self, col, proj_row, role): """ Return data for the tool tip role :param col: The column to display data for :type col: int :param proj_row: The project row object to display data for :type proj_row: ProjEntry :param role: The role to display data for. Must be Qt.ToolTipRole. (Note that this argument is ignored, but is present for consistency with the other data functions.) :type role: int :return: If a tool tip is to be displayed, return the tool tip data. Otherwise, return None. :rtype: str or NoneType """ if col == self.COLUMN.SPIN_MULT: ignored_spin_mult = proj_row.getIgnoredSpinMult() if ignored_spin_mult is not None: msg = ("The specified spin multiplicity (%i) is\n" "inconsistent with the current charge.\n" "The default spin multiplicity will be\n" "used instead." % ignored_spin_mult) return msg if col == self.COLUMN.BASIS: return self._basisToolTipData(proj_row) def _basisToolTipData(self, proj_row): """ Return data for the basis column tool tip :param proj_row: The project row object :type proj_row: ProjEntry :return: The tool tip text :rtype: str """ per_atom = self._getPerAtomBasis(proj_row) if per_atom: return self._perAtomToolTip(proj_row, per_atom) else: return self._singleBasisToolTip(proj_row) def _singleBasisToolTip(self, proj_row): """ Return the basis column tool tip for a structure without per-atom basis sets :param proj_row: The project row object :type proj_row: ProjEntry :return: The tool tip text :rtype: str """ basis = self._getBasis(proj_row) basis = str(basis) struc = proj_row.getStructure() (sentences, num_funcs) = basis_selector.generate_description(basis, struc) desc = basis_selector.combine_sentences(sentences[1:]) if not num_funcs: funcs_pre = self.ERROR_PRE funcs_post = self.ERROR_POST else: funcs_pre, funcs_post = "", "" msg = ("<table><tr><td>Basis set: </td><td>%s</td></tr>" "<tr><td>%sBasis functions: %s</td><td>%s%i%s</td></tr>" % (basis, funcs_pre, funcs_post, funcs_pre, num_funcs, funcs_post)) if desc: msg += "<tr><td colspan=2>%s</td></tr>" % desc msg += "</table>" return msg def _perAtomToolTip(self, proj_row, per_atom): """ Return the basis column tool tip for a structure with per-atom basis sets :param proj_row: The project row object :type proj_row: ProjEntry :param per_atom: The per-atom basis sets :type per_atom: dict :return: The tool tip text :rtype: str """ msg = ("<table><tr><th align=left>Atom</th>" "<th align=left>Basis Set</th></tr>") per_atom_by_name = self._getPerAtomBasis(proj_row, name=True) sorted_per_atom = sorted( per_atom_by_name.items(), key=lambda x: gui_utils.atom_name_sort_key(x[0])) pad_right = "style='padding-right: 10px;'" for cur_atom, cur_basis in sorted_per_atom: msg += "<tr><td %s>%s</td><td>%s</td></tr>" % (pad_right, cur_atom, cur_basis) if proj_row.basis is not None: basis = str(proj_row.basis) basis_fmt = basis else: basis = str(self.default_basis) basis_fmt = "<i>%s</i>" % basis msg += "<tr><td %s>all others</td><td>%s</td></tr></table>" % ( pad_right, basis_fmt) struc = proj_row.getStructure() num_funcs, is_ps = basis_selector.num_basis_functions(basis, struc, per_atom=per_atom) if not num_funcs: msg += self.ERROR_PRE msg += "Basis functions: %i" % num_funcs if not num_funcs: msg += self.ERROR_POST if not is_ps: msg += "<br>" + basis_selector.NO_PS_MSG return msg def _otherData(self, col, proj_row, role): """ Return data for the background or custom roles :param col: The column to display data for :type col: int :param proj_row: The project row object to display data for :type proj_row: ProjEntry :param role: The role to display data for. Must be Qt.BackgroundRole or a custom role (i.e. >= Qt.UserRole). :type role: int """ if role == Qt.BackgroundRole: # If adding red backgrounds for additional columns, note that you # may also need to change Qt.ForegroundRole (in _formattingData()) # to avoid using a gray font against a red background if col == self.COLUMN.BASIS: if not self._hasValidBasis(proj_row): return self.ERROR_BACKGROUND_BRUSH elif role == NUM_INCLUDED_ENTRIES_ROLE: if col == self.COLUMN.INCLUSION: return self._num_included_rows elif role == DEFAULT_ROLE: if col == self.COLUMN.CHARGE: return proj_row.row_charge elif col == self.COLUMN.SPIN_MULT: return proj_row.getDefaultSpinMult() elif role == STRUCTURE_ROLE: if col == self.COLUMN.BASIS: return proj_row.getStructure() elif role == PER_ATOM_BASIS_ROLE: if col == self.COLUMN.BASIS: return self._getPerAtomBasis(proj_row) elif role == Qt.EditRole: if col == self.COLUMN.BASIS: # We don't want to load the word "Custom" into the basis # selector line edit, so return the basis set here even if there # are per-atom basis sets basis = self._getBasis(proj_row) return str(basis) elif role == table_helper.ROW_OBJECT_ROLE: return proj_row def _getBasis(self, proj_row): """ Get the basis for the specified row :param proj_row: The project row object to get the basis for :type proj_row: ProjEntry :return: The basis for the specified row :rtype: `Basis` """ if proj_row.basis is not None: return proj_row.basis else: return self.default_basis def _getTheory(self, proj_row): if proj_row.theory is not None: return proj_row.theory else: return self.default_theory def _getDisplayBasis(self, proj_row, per_atom_name="Custom"): """ Get the basis set for the specified row. If there are per-atom basis sets for the specified row, return `per_atom_name` instead. :param proj_row: The project row object to get the basis for :type proj_row: ProjEntry :param per_atom_name: The basis set name to return if there are per-atom basis sets :type per_atom_name: str :return: The basis for the specified row or "Custom" :rtype: `Basis` or str """ per_atom = self._getPerAtomBasis(proj_row) if per_atom: return per_atom_name else: return self._getBasis(proj_row) def _getPerAtomBasis(self, proj_row, name=False): """ Get the per-atom basis for the specified row :param proj_row: The project row object :type proj_row: ProjEntry :param name: If True, the return dictionary keys will be atom names. If False, the keys will be atom numbers. :type name: bool :return: The per-atom basis sets for the specified row, or None if no per-atom basis set model has been specified :rtype: dict or NoneType """ if self._per_atom_basis_model is not None: eid = proj_row.entry_id return self._per_atom_basis_model.perAtomBasisForEid(eid, name=name) else: return None def _hasValidBasis(self, proj_row): """ Does the specified row have a valid basis set? :param proj_row: The project row object to check the basis for :type proj_row: ProjEntry :return: True if the basis for the specified row if valid. False otherwise. :rtype: bool """ struc = proj_row.getStructure() basis_name = str(self._getBasis(proj_row)) theory_name = str(self._getTheory(proj_row)) if theory_name == THEORY_RIMP2: return basis_name in self.valid_rimp2_basis_sets per_atom = self._getPerAtomBasis(proj_row) (num_functions, is_ps) = basis_selector.num_basis_functions(basis_name, struc, per_atom=per_atom) return bool(num_functions)
[docs] def headerData(self, section, orientation, role=Qt.DisplayRole): if orientation == Qt.Vertical: return if role == Qt.DisplayRole: return self.COLUMN.HEADERS[section]
[docs] def flags(self, index): col = index.column() if (col == self.COLUMN.INCLUSION and self._source is UseFrom.included): return Qt.ItemIsSelectable flag = Qt.ItemIsEnabled | Qt.ItemIsSelectable if col not in (self.COLUMN.ID, self.COLUMN.TITLE): flag |= Qt.ItemIsEditable return flag
[docs] def setData(self, index, value, role=Qt.EditRole): """ Set data for the specified index. The spin multiplicity tool tip will be shown if an invalid spin multiplicity is set, or if a charge is set that renders the spin multiplicity invalid. (The tool tip explains that the default spin multiplicity is being used because the user-specifed value is invalid.) :param index: The index to modify :type index: `PyQt5.QtCore.QModelIndex` :param value: The value to set :param role: The role to set data for. Must be Qt.EditRole or setting will fail. :type role: int :return: True if setting succeeded. False if it failed. :rtype: bool """ if role != Qt.EditRole: return False table_row = index.row() col = index.column() proj_row = self.cur_table_rows[table_row] if col == self.COLUMN.INCLUSION: proj_row.setIncluded(value) elif col == self.COLUMN.CHARGE: prev_acceptable_sm = proj_row.getIgnoredSpinMult() is None proj_row.charge = value new_acceptable_sm = proj_row.getIgnoredSpinMult() is None sm_index = self.index(table_row, self.COLUMN.SPIN_MULT) if prev_acceptable_sm and not new_acceptable_sm: self.show_tool_tip.emit(sm_index) elif col == self.COLUMN.SPIN_MULT: acceptable = proj_row.setSpinMult(value) if not acceptable: self.show_tool_tip.emit(index) elif col == self.COLUMN.BASIS: if value == str(self.default_basis): # PANEL-17212 - Don't explicitly set rows to the default value value = None proj_row.setBasisFromText(value) self.basisChanged.emit() elif col == self.COLUMN.THEORY: if value == self.default_theory: # PANEL-17212 - Don't explicitly set rows to the default value value = None proj_row.setMethodFromText(value) self.theoryChanged.emit() # If the charge has changed, the spin multiplicity may have changed as # well. if col == self.COLUMN.CHARGE: self.dataChanged.emit(index, sm_index) else: self.dataChanged.emit(index, index) return True
[docs] def checkBasisSets(self): """ Make sure that all structure have a valid basis set selected :return: A list of structures with invalid basis sets :rtype: list """ bad_strucs = [] for proj_row in self.cur_table_rows: if not self._hasValidBasis(proj_row): bad_strucs.append(proj_row.title) return bad_strucs
[docs] def setDefaultBasis(self, basis): """ Set the default basis set :param basis: The default basis set :type basis: str """ self.default_basis = Basis.fromText(basis) top_index = self.index(0, self.COLUMN.BASIS) bottom_index = self.index(self.rowCount() - 1, self.COLUMN.BASIS) self.dataChanged.emit(top_index, bottom_index)
[docs] def setDefaultTheory(self, theory): """ Set the default theory level/method :param theory: The default theory level/method :type theory: str """ self.default_theory = theory top_index = self.index(0, self.COLUMN.THEORY) bottom_index = self.index(self.rowCount() - 1, self.COLUMN.THEORY) self.dataChanged.emit(top_index, bottom_index)
[docs] def getStructures(self): """ Get a list of all structures loaded into the table (i.e. all structures selected in the project table) and the associated settings. :return: A list of ProjEntryTuple objects :rtype: list """ all_data = [] for proj_row in self.cur_table_rows: (eid, struc, charge, spin_mult) = \ proj_row.getStrucChargeAndSpinMult() basis = self._getBasis(proj_row) basis = str(basis) theory = self.getMethodForEid(eid) cur_data = ProjEntryTuple(eid, struc, charge, spin_mult, basis, theory) all_data.append(cur_data) return all_data
[docs] def reset(self): """ Reset all charge, spin multiplicity, and basis settings """ proj = maestro.project_table_get() for entry_id in list(self.all_rows): row = proj.getRow(entry_id) if row is not None: # If a row still exists in the project table, reset it self.all_rows[entry_id].reset() else: # If it no longer exists, delete it del self.all_rows[entry_id] top_index = self.index(0, 0) bottom_index = self.index(self.rowCount(), self.columnCount()) self.dataChanged.emit(top_index, bottom_index)
[docs] def resetBasisForRows(self, indices): """ Reset the basis set to the default for the specified indices. :param indices: Indices to be Reset :type indices: list(QModelIndex) """ proj_rows = [i.data(table_helper.ROW_OBJECT_ROLE) for i in indices] for row in proj_rows: row.basis = None for index in indices: self.dataChanged.emit(index, index) self.basisChanged.emit()
[docs] def resetTheoryForRows(self, indices): """ Reset the method to the default for the specified indices. :param indices: Indices to be Reset :type indices: list(QModelIndex) """ proj_rows = [i.data(table_helper.ROW_OBJECT_ROLE) for i in indices] for row in proj_rows: row.theory = None for index in indices: self.dataChanged.emit(index, index) self.theoryChanged.emit()
[docs] def getCommonBasis(self): """ If all structures use the same basis set, return the basis set name. Otherwise, return None. :note: The basis set returned here is not guaranteed to be the default basis set. The user may have specified identical per-structure basis sets for all structures. :return: The basis set name or None :rtype: str or NoneType """ basis_sets = [self._getBasis(row) for row in self.cur_table_rows] basis0 = basis_sets[0] if basis_sets.count(basis0) == len(basis_sets): return str(basis0) else: return None
[docs] def getCommonMethod(self): """ If all structures use the same method, return the method name. Otherwise, return None. :note: The method returned here is not guaranteed to be the default method. The user may have specified identical per-structure methods for all structures. :return: The method name or None :rtype: str or NoneType """ methods = set( self.getMethodForEid(row.entry_id) for row in self.cur_table_rows) return methods.pop() if len(methods) == 1 else None
[docs] def getCommonTheoryLevel(self): """ :return: If all current methods are of the same theory level, return the theory level. Otherwise return None. :rtype: str or None """ methods = set( self.getMethodForEid(row.entry_id) for row in self.cur_table_rows) if THEORY_HF in methods or THEORY_LMP2 in methods or \ THEORY_RIMP2 in methods: return methods.pop() if len(methods) == 1 else None return THEORY_DFT
[docs] def chargedStrucsPresent(self): """ Determine if the user has specified any molecular charges :return: True if the user has specified a molecular charge for any molecule. False otherwise. :rtype: bool """ return any([row.getCharge()[0] for row in self.cur_table_rows])
[docs] def entryTitles(self): """ Get a dictionary of {entry id: entry title} for all selected entries """ return {row.entry_id: row.title for row in self.cur_table_rows}
[docs] def entryIds(self): """ Get a set of entry ids for all selected entries """ return {row.entry_id for row in self.cur_table_rows}
[docs] def getBasisForEid(self, eid, per_atom_name): """ Get the basis set for the specified entry id. If there are per-atom basis sets specified for the structure, the `per_atom_name` will be returned. :param eid: The entry id :type eid: str :param per_atom_name: The name to return if per-atom basis sets are specified :type per_atom_name: str :return: The basis set name, or `per_atom_name` if per-atom basis sets are specified :rtype: `Basis` or str """ proj_row = self.all_rows[eid] return self._getDisplayBasis(proj_row, per_atom_name)
[docs] def getMethodForEid(self, eid): """ Return the method for the specified entry ID. :param eid: Entry ID to get method for. :type eid: int :return: Method for this entry ID. :rtype: str """ proj_row = self.all_rows[eid] if proj_row.theory is None: return self.default_theory return proj_row.theory
[docs] def setSource(self, source): """ Specify the row source, i.e., included entries or selected entries :param source: The row source :type source: `UseFrom` or `schrodinger.ui.qt.input_selector.InputSelector` class constant that maps to a UseFrom enum """ try: # InputSelector data from the Multistage panel is just a string # rather than a UseFrom enum, so attempt to map the input to a # UseFrom enum source = SELECTOR_TO_USEFROM[source] except KeyError: # source is already a UseFrom enum (or some unknown data) pass if source is UseFrom.included: self._row_func = lambda proj: proj.included_rows elif source is UseFrom.selected: self._row_func = lambda proj: proj.selected_rows else: raise ValueError("Invalid source: %s" % source) self._source = source self.projectUpdated() # Notify the view that the flags have changed for the Included column top = self.index(0, self.COLUMN.INCLUSION) bottom = self.index(self.rowCount(), self.COLUMN.INCLUSION) self.dataChanged.emit(top, bottom)
[docs] def source(self): """ Return the row source, i.e., included entries or selected entries :return: The row source :rtype: `UseFrom` """ return self._source
[docs] def usingSelected(self): """ Return True if the tab is set to use selected entries. False if the tab is set to use included entries. """ return self._source == UseFrom.selected
[docs]class InputEntriesProxyModel(table_helper.PythonSortProxyModel): """ The proxy model for sorting the input entries table :ivar show_tool_tip: A signal indicating that the tool tip for the specified cell should explicitly be shown. :vartype show_tool_tip: `PyQt5.QtCore.pyqtSignal` :ivar redraw: A signal to request the view to repaint. This shouldn't be necessary, but the view doesn't repaint without it. :vartype redraw: `PyQt5.QtCore.pyqtSignal` """ show_tool_tip = QtCore.pyqtSignal(QtCore.QModelIndex) redraw = QtCore.pyqtSignal() COLUMN = None SORT_ROLE = SORT_ROLE
[docs] def setSourceModel(self, model): super(InputEntriesProxyModel, self).setSourceModel(model) self.COLUMN = model.COLUMN model.show_tool_tip.connect(self.promoteShowToolTip) model.dataChanged.connect(self.redraw.emit) # maintain Qt4 dynamicSortFilter default self.setDynamicSortFilter(False)
@property def default_basis(self): return self.sourceModel().default_basis @property def default_theory(self): return self.sourceModel().default_theory
[docs] def promoteShowToolTip(self, model_index): """ When a show_tool_tip signal is received from the model, translate the index and re-emit the signal. :param model_index: The model index :type model_index: `PyQt5.QtCore.QModelIndex` """ index = self.mapFromSource(model_index) self.show_tool_tip.emit(index)
[docs] def source(self): """ Return the row source, i.e., included entries or selected entries :return: The row source :rtype: `UseFrom` """ return self.sourceModel().source()
[docs] def resetBasisForRows(self, indices): """ Reset the basis set to the default for the specified indices. :param indices: Indices to be Reset :type indices: list(QModelIndex) """ source_indices = [self.mapToSource(i) for i in indices] source_model = self.sourceModel() source_model.resetBasisForRows(source_indices)
[docs] def resetTheoryForRows(self, indices): """ Reset the method to the default for the specified indices. :param indices: Indices to be Reset :type indices: list(QModelIndex) """ source_indices = [self.mapToSource(i) for i in indices] source_model = self.sourceModel() source_model.resetTheoryForRows(source_indices)
[docs]class InputEntriesView(gui_utils.ProjTableLikeView): """ The view for the input entries table :ivar basisFiltersChanged: Signal emitted when basis set filters are toggled. emits a dict of current filter settings. :type basisFiltersChanged: QtCore.pyQtSignal(dict) :ivar methodFiltersChanged: Signal emitted when method filters are toggled. emits a dict of current filter settings. :type methodFiltersChanged: QtCore.pyQtSignal(dict) """ COLUMN = InputEntriesColumns basisFiltersChanged = QtCore.pyqtSignal(dict) methodFiltersChanged = QtCore.pyqtSignal(dict)
[docs] def __init__(self, parent): """ Instantiate the view, connect the appropriate delegates, and set the selection behavior to mimic the project table. """ super(InputEntriesView, self).__init__(parent) self._setDelegates(parent) self.setSortingEnabled(True) self.sortByColumn(self.COLUMN.ID, Qt.AscendingOrder) self.setSelectionBehavior(self.SelectRows) self.setSelectionMode(self.ExtendedSelection) edit_triggers = self.editTriggers() edit_triggers |= self.SelectedClicked self.setEditTriggers(edit_triggers) self._menu = QtWidgets.QMenu(self) self._menu.addAction("Remove", self._removeRows)
def _setDelegates(self, parent): """ Add delegates to the appropriate columns :param parent: The Qt parent of this table :type parent: `PyQt5.QtWidgets.QWidget` """ self._setInclusionDelegate(parent) self._setChargeAndSpinMultDelegates(parent) self._setBasisDelegate(parent) self._setTheoryDelegate(parent) def _setInclusionDelegate(self, parent): """ Add the delegate to the inclusion checkbox column :param parent: The Qt parent of this table :type parent: `PyQt5.QtWidgets.QWidget` """ self._inclusion_delegate = qt_delegates.WorkspaceInclusionDelegate( parent) self.setItemDelegateForColumn(self.COLUMN.INCLUSION, self._inclusion_delegate) def _setChargeAndSpinMultDelegates(self, parent): """ Add delegates to the charge and spin multiplicity columns :param parent: The Qt parent of this table :type parent: `PyQt5.QtWidgets.QWidget` """ self._sb_delegate = BlankableSpinBoxDelegate(parent) self._sb_delegate.commitDataToSelected.connect( self.commitDataToSelected) self.setItemDelegateForColumn(self.COLUMN.CHARGE, self._sb_delegate) self.setItemDelegateForColumn(self.COLUMN.SPIN_MULT, self._sb_delegate) def _setBasisDelegate(self, parent): """ Add the delegate to the basis column :param parent: The Qt parent of this table :type parent: `PyQt5.QtWidgets.QWidget` """ self._basis_delegate = BasisSetDelegate(parent) self._basis_delegate.commitDataToSelected.connect( self.commitDataToSelected) self._basis_delegate.filtersChanged.connect(self.basisFiltersChanged) self.setItemDelegateForColumn(self.COLUMN.BASIS, self._basis_delegate) def _setTheoryDelegate(self, parent): """ Add the delegate to the theory column :param parent: The Qt parent of this table :type parent: PyQt5.QtWidgets.QWidget """ self._theory_delegate = MethodDelegate(parent) self._theory_delegate.commitDataToSelected.connect( self.commitDataToSelected) self._theory_delegate.filtersChanged.connect( self.methodFiltersChanged.emit) self.setItemDelegateForColumn(self.COLUMN.THEORY, self._theory_delegate)
[docs] def applyBasisSetFilterSettings(self, settings): """ Apply the specified basis set filter settings to the basis set delegate. :param settings: Basis set filter settings to apply. :type settings: dict """ self._basis_delegate.applyBasisSetFilterSettings(settings, False)
[docs] def applyMethodFilterSettings(self, settings): """ Apply the specified method filter settings to the basis set delegate. :param settings: Method filter settings to apply. :type settings: dict """ self._theory_delegate.applyMethodFilterSettings(settings, False)
[docs] def showToolTip(self, index): """ Show the tool tip for the specified index in response to the model emitting a show_tool_tip signal. (Note that this function has nothing to do with tool tips being shown when the user hovers over a cell.) :param index: The index to show the tool tip for :type index: `PyQt5.QtCore.QModelIndex` """ rect = self.visualRect(index) center = rect.center() center = self.viewport().mapToGlobal(center) text = index.data(Qt.ToolTipRole) tt_func = lambda: QtWidgets.QToolTip.showText(center, text, self) QtCore.QTimer.singleShot(0, tt_func)
[docs] def setModel(self, model): super(InputEntriesView, self).setModel(model) model.show_tool_tip.connect(self.showToolTip) # Explicitly repaint when the model updates. This shouldn't be # necessary, but the view doesn't properly repaint without it. model.redraw.connect(self.viewport().update)
[docs] def contextMenuEvent(self, event): # See Qt documentation for method documentation self._menu.popup(event.globalPos())
def _removeRows(self): """ Remove all highlighted rows from the table by un-selecting or un- including them in the project table. """ proj = maestro.project_table_get() source = self.model().source() in_indices = self.selectionModel().selectedRows(self.COLUMN.ID) for cur_index in in_indices: eid = cur_index.data() proj_row = proj[eid] if source is UseFrom.included: proj_row.in_workspace = project.NOT_IN_WORKSPACE elif source is UseFrom.selected: proj_row.is_selected = False
[docs] def selectionContainsNonDefaultBasisSets(self): """ :return: True if the current selection contains a non-default basis set, False otherwise. :rtype: bool """ basis_indices = self.selectionModel().selectedRows(self.COLUMN.BASIS) if not basis_indices: return False model = self.model() default_basis = str(model.default_basis) return not all(str(i.data()) == default_basis for i in basis_indices)
[docs] def resetBasisOfSelectedRows(self): """ Reset the basis set of the currently selected rows. """ sel_row_indices = self.selectionModel().selectedRows() self.model().resetBasisForRows(sel_row_indices)
[docs] def selectionContainsNonDefaultMethod(self): """ :return: True if the current selection contains a non-default theory level, False otherwise. :rtype: bool """ theory_indices = self.selectionModel().selectedRows(self.COLUMN.THEORY) if not theory_indices: return False model = self.model() default_theory = model.default_theory return any(i.data() != default_theory for i in theory_indices)
[docs] def resetTheoryOfSelectedRows(self): """ Reset the theory level of the currently selected rows. """ sel_row_indices = self.selectionModel().selectedRows() self.model().resetTheoryForRows(sel_row_indices)
[docs]class CommitMultipleDelegate(QtWidgets.QStyledItemDelegate): """ A delegate where Ctrl+Enter will cause the value to be committed to all selected rows. Note that the editor must have an index attribute containing the index being edited. (This index is needed for view.commitDataToSelected(). Qt itself uses QAbstractItemViewPrivate.indexForEditor() to retrieve the index. We don't have access to the QAbstractItemViewPrivate class, though, so we have store the index in the editor instead.) :ivar commitDataToSelected: Commit the data from the current editor to all selected cells. This signal is emitted with the editor, the current index, and the delegate. :vartype commitDataToSelected: `PyQt5.QtCore.pyqtSignal` """ commitDataToSelected = QtCore.pyqtSignal(QtWidgets.QWidget, QtCore.QModelIndex, QtWidgets.QAbstractItemDelegate)
[docs] def eventFilter(self, editor, event): """ Handle Ctrl+Enter """ key_press = (event.type() == event.KeyPress and event.key() in (Qt.Key_Return, Qt.Key_Enter)) ctrl = key_press and event.modifiers() & Qt.ControlModifier if ctrl: self.commitDataToSelected.emit(editor, editor.index, self) self.closeEditor.emit(editor, self.NoHint) return True else: return super(CommitMultipleDelegate, self).eventFilter(editor, event)
[docs]class BlankableSpinBoxDelegate(CommitMultipleDelegate): """ A spin box delegate. If the spin box is committed while it is blank, model.setData will be called with a value of None. """
[docs] def createEditor(self, parent, option, index): default = index.data(DEFAULT_ROLE) editor = BlankableSpinBox(parent, default) editor.index = index return editor
[docs] def setEditorData(self, editor, index): value = index.data() editor.setValue(value)
[docs] def setModelData(self, editor, model, index): editor.interpretText() value = editor.value() model.setData(index, value)
[docs]class BlankableSpinBox(QtWidgets.QSpinBox): """ A spin box that allows the empty string as an acceptable value """ # Based on: http://qt-project.org/faq/answer/how_can_i_set_an_empty_ # default_value_in_qspinbox
[docs] def __init__(self, parent, default=0): """ Initialize the spin box with a range from -99 to 99. -100 is used as the sentinel value for empty string. :param parent: The Qt parent :type parent: `PyQt5.QtWidgets.QWidget` :param default: The default value, i.e. what value should we starting counting from if the user clears the spin box and then increments. :type default: int """ super(BlankableSpinBox, self).__init__(parent) self.setMinimum(-100) self.setMaximum(99) self.default = default
[docs] def valueFromText(self, text): """ Convert the specified text to an integer. "" is converted to -100. :param text: The text to convert :type text: str :return: The converted text :rtype: `PyQt5.QtCore.QInt` """ if not text: return self.minimum() else: return super(BlankableSpinBox, self).valueFromText(text)
[docs] def textFromValue(self, value): """ Convert the specified integer to text. -100 is converted to "". :param value: The integer to convert :type value: int :return: The converted integer :rtype: str """ if value == self.minimum(): return "" else: return super(BlankableSpinBox, self).textFromValue(value)
[docs] def stepBy(self, steps): """ Increment the value of the spin box by the specified amount. If the spin box contains "" before incrementing, load the default value. :param steps: The value to increment the spin box by :type steps: int """ if self.value() is None: self.setValue(self.default) return super(BlankableSpinBox, self).stepBy(steps)
[docs] def validate(self, input_text, pos): """ Is the provided input acceptable? The blank string is considered acceptable. See PyQt documentation for argument and return value documentation. """ if not input_text: return (QtGui.QValidator.Acceptable, input_text, pos) else: return super(BlankableSpinBox, self).validate(input_text, pos)
[docs] def value(self): """ Return the current value in the spin box. If the spin box is blank, None is returned. :return: The current value in the spin box :rtype: int or NoneType """ value = super(BlankableSpinBox, self).value() if value == self.minimum(): return None else: return value
[docs] def stepEnabled(self): """ Report on whether stepping up and down is allowed. When the spin box is blank, the user can step both up and down. The user cannot step down to the minimum, since that is a sentinel value. :return: A flag indicating whether stepping is allowed :rtype: int """ value = self.value() if value is None: return self.StepUpEnabled | self.StepDownEnabled elif value == self.minimum() + 1: return self.StepUpEnabled else: return super(BlankableSpinBox, self).stepEnabled()
[docs]class BasisSetDelegate(pop_up_widgets.PopUpDelegate): """ A delegate for selecting a basis set using a BasisSelectorLineEdit. Additionally, Ctrl+Enter will cause the current value to be committed to all selected rows. :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(BasisSetDelegate, self).__init__(parent, None, True) self._basis_filter_settings = {}
def _createEditor(self, parent, option, index): # See parent class and Qt documentation struc = index.data(STRUCTURE_ROLE) per_atom = index.data(PER_ATOM_BASIS_ROLE) editor = basis_selector.FilterBasisSelectorReadOnlyLineEdit(parent) editor.applySettings(self._basis_filter_settings) editor.setBasis() editor.setStructure(struc) editor.setPerAtom(per_atom) editor.filtersChanged.connect(self.applyBasisSetFilterSettings) return editor
[docs] def setEditorData(self, editor, index): # See Qt documentation for an explanation of arguments # Use the EditRole so we get the basis set name even if there are # per-atom basis sets. (The DisplayRole would return the word "Custom" # instead.) value = index.data(Qt.EditRole) editor.setText(str(value))
[docs] def applyBasisSetFilterSettings(self, settings, emit_filters_changed=True): """ Apply the specified basis set filter settings to the basis set popup. :param settings: Basis set filter settings to apply. :type settings: dict :param emit_filters_changed: Whether to emit the filtersChanged signal :type emit_filters_changed: bool """ self._basis_filter_settings = settings if emit_filters_changed: self.filtersChanged.emit(settings)
[docs]class MethodDelegate(pop_up_widgets.PopUpDelegate): """ A delegate for selecting a method. Additionally, Ctrl+Enter will cause the current value to be committed to all selected rows. :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, None, True) self._method_filter_settings = {}
def _createEditor(self, parent, option, index): # See parent class and Qt documentation editor = theory_selector.FilterTheorySelectorReadOnlyLineEdit(parent) editor.applySettings(self._method_filter_settings) editor.setMethod() editor.filtersChanged.connect(self.applyMethodFilterSettings) return editor
[docs] def applyMethodFilterSettings(self, settings, emit_filters_changed=True): """ Apply the specified method filter settings to the basis set popup. :param settings: Method filter settings to apply. :type settings: dict :param emit_filters_changed: Whether to emit the filtersChanged signal :type emit_filters_changed: bool """ self._method_filter_settings = settings if emit_filters_changed: self.filtersChanged.emit(settings)
[docs]class UseFromCombo(swidgets.SComboBox): """ A combo box that allows the user to select the source for the project table: either included entries or selected entries. """
[docs] def __init__(self, parent=None): # See QComboBox documentation for argument documentation super(UseFromCombo, self).__init__(parent) self.addItem("", UseFrom.included) self.addItem("", UseFrom.selected) proj = maestro.project_table_get() self.updateText(proj)
[docs] def updateText(self, proj): """ Update the combo box text to reflect the current number of included and selected entries. This method must be called every time there's a project change. :param proj: The Maestro project :type proj: `schrodinger.project.Project` """ num_included = len(proj.included_rows) num_selected = len(proj.selected_rows) plural = lambda x: "y" if x == 1 else "ies" incl_txt = ("Workspace (%i included entr%s)" % (num_included, plural(num_included))) sel_txt = ("Project Table (%i selected entr%s)" % (num_selected, plural(num_selected))) self.setItemText(0, incl_txt) if self.count() > 1: self.setItemText(1, sel_txt)
[docs] def removeSelectedEntriesItem(self): """ Removes 'Selected entries' item from the menu. """ idx = self.findDataPy(UseFrom.selected) self.removeItem(idx)