Source code for schrodinger.application.jaguar.gui.tabs.input_tab

import warnings

import schrodinger
from schrodinger import project
from schrodinger.application.jaguar import basis as jag_basis
from schrodinger.infra import mm
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 utils as qt_utils

from .. import input_tab_widgets
from .. import input_tab_widgets_hb
from .. import input_tab_widgets_pka
from .. import ui
from .. import utils as gui_utils
from ..utils import THEORY_DFT
from ..utils import THEORY_HF
from ..utils import THEORY_LMP2
from ..utils import THEORY_RIMP2
from ..utils import THEORY_MIXED
from ..utils import JaguarSettingWarning
from . import input_sub_tabs
from .base_tab import BaseTab
from .base_tab import ProvidesBasisMixin
from .base_tab import ProvidesStructuresMixin
from .base_tab import ProvidesTheoryMixin

maestro = schrodinger.get_maestro()


[docs]class InputTabBase(ProvidesStructuresMixin, BaseTab): """ A tab for specifying settings for the structures currently selected in the project table. Note that this class is not intended to be directly instantiated. Instead, the InputTab or InputTabPka classes should be used. :cvar MODEL_CLASS: The class to use for the table model :vartype MODEL_CLASS: `schrodinger.application.jaguar.gui.input_tab_widgets. SelectedEntriesModel` :cvar PROXY_CLASS: The class to use for the table proxy model :vartype PROXY_CLASS: `schrodinger.application.jaguar.gui.input_tab_widgets. SelectedEntriesProxyModel` :cvar VIEW_CLASS: The class to use for the table view :vartype VIEW_CLASS: `schrodinger.application.jaguar.gui.input_tab_widgets. SelectedEntriesView` :cvar strucSourceChanged: A signal emitted when the structure source changes from included to selected or vice versa. :vartype strucSourceChanged: `QtCore.pyqtSignal` """ strucSourceChanged = QtCore.pyqtSignal() NAME = "Input" MODEL_CLASS = input_tab_widgets.InputEntriesModel PROXY_CLASS = input_tab_widgets.InputEntriesProxyModel VIEW_CLASS = input_tab_widgets.InputEntriesView # The strucSourceChanged signal gets added by the metaclass to work around a # limitation in PyQt's multiple inheritance abilities # strucSourceChanged = QtCore.pyqtSignal()
[docs] def setup(self): model = self.MODEL_CLASS(self) self._input_entries_model = model proxy_model = self.PROXY_CLASS(self) proxy_model.setSourceModel(model) self.input_entries_view = self.VIEW_CLASS(self) self.ui.table_view_layout.addWidget(self.input_entries_view) self.input_entries_view.setModel(proxy_model) self.input_entries_view.resizeColumnsToContents() self.ui.use_from_combo.currentIndexChanged.connect( self.useFromComboChanged)
[docs] def projectUpdated(self): """ When the project is updated, update the selected entries table """ try: proj = maestro.project_table_get() except project.ProjectException: # If the project was just closed self._input_entries_model.clearRows() return self.ui.use_from_combo.updateText(proj) self._input_entries_model.projectUpdated() self.structureChanged.emit()
[docs] def workspaceChanged(self, what_changed): """ Update the table whenever the workspace changes 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 """ self._input_entries_model.workspaceChanged(what_changed)
[docs] def loadSettings(self, jag_input): self._loadChargeAndSpinMult(jag_input)
def _loadChargeAndSpinMult(self, jag_input): """ Load charge and spin multiplicity settings from mmjag keywords. If the relevant settings are at their defaults, then no changes will be made. If the relevant settings are not defaults, then there must be exactly one structure loaded into the table and the settings will be applied to this structure. :param jag_input: A JaguarInput object containing the settings to load :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` """ if not (jag_input.isNonDefault(mm.MMJAG_IKEY_MOLCHG) or jag_input.isNonDefault(mm.MMJAG_IKEY_MULTIP)): # Don't bother to do anything if the settings are defaults return elif not self._checkSingleStructure("charge and spin multiplicity"): return model = self._input_entries_model charge = jag_input[mm.MMJAG_IKEY_MOLCHG] charge_index = model.index(0, model.COLUMN.CHARGE) model.setData(charge_index, charge) spin_mult = jag_input[mm.MMJAG_IKEY_MULTIP] spin_mult_index = model.index(0, model.COLUMN.SPIN_MULT) model.setData(spin_mult_index, spin_mult) def _checkSingleStructure(self, setting_name): """ Make sure that there is only a single structure loaded into the table and issue a warning if this isn't the case. :param setting_name: The name of the setting that we're trying to load. This name is only used in the text of the warning. :type setting_name: str :return: True if there is exactly one structure loaded into the table. False otherwise. :rtype: bool """ row_count = self._input_entries_model.rowCount() if row_count == 1: return True else: msg = "Cannot load %s when " % setting_name if row_count == 0: msg += "no structures are loaded." else: msg += "more than one structure is loaded." warnings.warn(JaguarSettingWarning(msg)) return False
[docs] def validate(self): if self._input_entries_model.rowCount() == 0: return "No structures are selected."
[docs] def getStructures(self): """ Get all structures loaded into the tab and their associated Jaguar settings :return: A list of tuples. Each tuple represents a single structure and contains: - Entry ID of the structure - A `schrodinger.structure.Structure` object containing the structure itself - A dictionary of keywords specific to the structure :rtype: list """ data_from_table = self._input_entries_model.getStructures() data_to_tab = [] for cur_struc in data_from_table: keywords = self._getKeywordsForStruc(cur_struc) cur_to_tab = (cur_struc.entry_id, cur_struc.struc, keywords) data_to_tab.append(cur_to_tab) return data_to_tab
def _getKeywordsForStruc(self, cur_struc): """ Get mmjag keywords for the specified structure :param cur_struc: Data about the current structure :type cur_struc: `schrodinger.application.jaguar.gui.input_tab_widgets. ProjEntryTuple` :return: A dictionary of Jaguar keywords :rtype: dict """ raise NotImplementedError
[docs] def getNumStructures(self): return self._input_entries_model.rowCount()
[docs] def getStructureTitleForJobname(self): # See ProvidesStructuresMixin for method documentation num_strucs = self.getNumStructures() if num_strucs == 0: return None elif num_strucs == 1: entry_titles = self._input_entries_model.entryTitles() return list(entry_titles.values())[0] else: return self.MULTIPLE_STRUC_JOB_TITLE
[docs] def reset(self): """ Reset any structure specific settings in the table. (The non-struture specific settings will be reset via loadSettings()) """ self._input_entries_model.reset()
[docs] def useFromComboChanged(self): """ Respond to the user changing the structure source (from included to selected or vice versa). """ source = self.ui.use_from_combo.currentData() self._input_entries_model.setSource(source) self.strucSourceChanged.emit()
[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._input_entries_model.usingSelected()
[docs]class InputTab(ProvidesTheoryMixin, ProvidesBasisMixin, InputTabBase): """ An input tab that allows the user to specify basis sets. """ HELP_TOPIC = "JAGUAR_INPUT_TAB" UI_MODULES = (ui.input_top_ui, ui.input_std_bottom_ui)
[docs] def setup(self): super().setup() basis_btn = self.ui.basis_chooser_btn basis_btn.setPopupValign(basis_btn.ALIGN_BOTTOM) basis_btn.popUpClosing.connect(self._checkNewBasis) basis_btn.filtersChanged.connect( self.input_entries_view.applyBasisSetFilterSettings) theory_btn = self.ui.theory_chooser_btn theory_btn.setPopupValign(theory_btn.ALIGN_BOTTOM) theory_btn.popUpClosing.connect(self._checkNewTheory) theory_btn.filtersChanged.connect( self.input_entries_view.applyMethodFilterSettings) self._acceptable_basis = "" self._input_entries_model.basisChanged.connect(self.basis_changed.emit) self._input_entries_model.theoryChanged.connect( self.method_changed.emit) self.input_entries_view.selectionModel().selectionChanged.connect( self._onInputEntriesSelectionChanged) self.input_entries_view.basisFiltersChanged.connect( basis_btn.applySettings) self.input_entries_view.methodFiltersChanged.connect( theory_btn.applySettings) self.ui.clear_basis_btn.clicked.connect( self.input_entries_view.resetBasisOfSelectedRows) self.ui.clear_theory_btn.clicked.connect( self.input_entries_view.resetTheoryOfSelectedRows)
def _checkNewBasis(self): """ When the user enters a new default basis set, check to see if it's acceptable. If it is, update self._acceptable_basis and emit basis_changed. """ self.ui.default_basis_le.setText(self.ui.basis_chooser_btn.getBasis()) if self.ui.basis_chooser_btn.hasAcceptableInput(): basis = self.ui.default_basis_le.text() self._acceptable_basis = str(basis) self._input_entries_model.setDefaultBasis(basis) self.basis_changed.emit() def _checkNewTheory(self): """ Handle user selection of new default theory/method. """ theory = self.ui.theory_chooser_btn.getMethod() self.ui.default_theory_le.setText(theory) self._input_entries_model.setDefaultTheory(theory) self.method_changed.emit()
[docs] def getDefaultKeywords(self): """ The mmjag default basis set is "", which would cause a warning, so we set the default basis. Additionally, the mmjag default DFT functional name is "", which would cause a HF level of theory. Instead, set a default functional of B3LYP-D3. :return: A keyword dictionary with the default basis set and functional. :rtype: dict """ return { mm.MMJAG_SKEY_BASIS: jag_basis.default_basis(), mm.MMJAG_SKEY_DFTNAME: 'B3LYP-D3' }
[docs] def loadSettings(self, jag_input): super().loadSettings(jag_input) self._loadBasis(jag_input) theory_level = self._determineTheoryLevel(jag_input) self._loadAndResetFunctional(jag_input, theory_level)
def _loadBasis(self, jag_input): """ Load the default basis from the mmjag keywords :param jag_input: A JaguarInput object containing the settings to load :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` """ basis = jag_input[mm.MMJAG_SKEY_BASIS] basis = basis.upper() if not basis: warnings.warn(JaguarSettingWarning("No basis set was specified.")) else: try: self.ui.basis_chooser_btn.setBasis(basis) self._checkNewBasis() except ValueError: msg = ("The specified basis set (%s=%s) was not recognized." % (mm.MMJAG_SKEY_BASIS, basis)) warnings.warn(JaguarSettingWarning(msg))
[docs] def getBasis(self, mixed_name="Mixed"): # See ProvidesBasisMixin for method documentation if self._input_entries_model.rowCount(): basis = self._input_entries_model.getCommonBasis() if basis is None: basis = mixed_name return basis else: return self._acceptable_basis
[docs] def getTheoryLevel(self): """ Get the current theory level :return: The current theory level. :rtype: str """ if self._input_entries_model.rowCount(): theory = self._input_entries_model.getCommonMethod() if theory is None: theory = THEORY_MIXED else: theory = self.ui.default_theory_le.text() if theory not in [THEORY_HF, THEORY_LMP2, THEORY_RIMP2]: theory = THEORY_DFT return theory
[docs] def getAllUsedTheoryLevels(self): common_theory_level = self.getTheoryLevel() if common_theory_level != THEORY_MIXED: return (common_theory_level,) theory_levels = set() for eid, _, _ in self.getStructures(): theory_levels.update(self.getTheoryLevelForEid(eid)) return tuple(theory_levels)
[docs] def getMethod(self): """ Get the current method. :return: The current method :rtype: str """ if self._input_entries_model.rowCount() == 0: return self.ui.default_theory_le.text() method = self.getCommonMethod() if method is None: return THEORY_MIXED return method
[docs] def getFunctional(self, cur_struc=None): """ Get the current DFT functional. :param cur_struc: Current structure to get functional for. If not specified, functional will be returned based on default theory setting. :type cur_struc: input_tab_widgets.ProjEntryTuple :return: The current DFT functional :rtype: str or None """ if cur_struc is None or cur_struc.theory is None: theory = self.ui.default_theory_le.text() else: theory = cur_struc.theory if theory in [THEORY_HF, THEORY_LMP2, THEORY_RIMP2]: return None return theory
def _loadAndResetFunctional(self, jag_input, theory_level): """ If the theory level is DFT, load the specified functional from the mmjag handle. If the theory level isn't DFT, load the theory itself. :param jag_input: A JaguarInput object containing the settings to load :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` :param theory_level: The level of theory to load the settings into. Must be either "DFT", "HF", "LMP2" or "RIMP2". :type theory_level: str """ if theory_level == THEORY_DFT: self._loadFunctional(jag_input) else: if self.ui.theory_chooser_btn.isItemHidden(theory_level): self.ui.theory_chooser_btn.clearFilters() success = self.ui.theory_chooser_btn.setMethod(theory_level) if not success: raise ValueError(f"Unrecognized theory level {theory_level}") self._checkNewTheory() def _setFunctional(self, functional): """ Set the GUI to the specified DFT functional :param functional: The functional to set :type functional: str :raise ValueError: If the specified functional is not present in the GUI """ if self.ui.theory_chooser_btn.isItemHidden(functional): self.ui.theory_chooser_btn.clearFilters() success = self.ui.theory_chooser_btn.setMethod(functional) if not success: raise ValueError("Unknown functional.") self._checkNewTheory() def _getKeywordsForStruc(self, cur_struc): functional = self.getFunctional(cur_struc) return { mm.MMJAG_IKEY_MOLCHG: cur_struc.charge, mm.MMJAG_IKEY_MULTIP: cur_struc.spin_mult, mm.MMJAG_SKEY_BASIS: cur_struc.basis, mm.MMJAG_SKEY_DFTNAME: functional }
[docs] def validate(self): retval = super().validate() if retval: return retval if not self.ui.basis_chooser_btn.hasAcceptableInput(): default_basis = self.ui.default_basis_le.text() return ("The specified default basis set (%s) is invalid." % default_basis) bad_strucs = self._input_entries_model.checkBasisSets() if bad_strucs: plural_s = "s" if len(bad_strucs) > 1 else "" have_s = "have" if len(bad_strucs) > 1 else "has" struc_list = ", ".join(bad_strucs) return ("The following structure%s %s an invalid basis set / " "theory combination: %s." % (plural_s, have_s, struc_list))
[docs] def getCommonBasis(self): """ If all structures use the same basis set, return the basis set name. Otherwise, return None. :return: The basis set name or None :rtype: str or NoneType """ return self._input_entries_model.getCommonBasis()
[docs] def getCommonMethod(self): """ If all structures use the same method, return the method name. Otherwise, return None. :return: The method name or None :rtype: str or NoneType """ return self._input_entries_model.getCommonMethod()
[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 """ return self._input_entries_model.getCommonTheoryLevel()
[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 self._input_entries_model.chargedStrucsPresent()
[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 """ return self._input_entries_model.getBasisForEid(eid, per_atom_name)
[docs] def getTheoryLevelForEid(self, eid): """ Get the theory level for the specified entry ID. :param eid: Entry ID to get theory level of. :type eid: int :return: Theory level for this entry ID. :rtype: str """ method = self._input_entries_model.getMethodForEid(eid) return method if method in [THEORY_HF, THEORY_LMP2, THEORY_RIMP2 ] else THEORY_DFT
[docs] def getMethodForEid(self, eid): """ Get the method for the specified entry ID. :param eid: Entry ID to get method of. :type eid: int :return: Method for this entry ID. :rtype: str """ return self._input_entries_model.getMethodForEid(eid)
def _onInputEntriesSelectionChanged(self): """ Respond to selection changes in the input entries table. """ enable_clear_basis = \ self.input_entries_view.selectionContainsNonDefaultBasisSets() self.ui.clear_basis_btn.setEnabled(enable_clear_basis) enable_clear_theory = \ self.input_entries_view.selectionContainsNonDefaultMethod() self.ui.clear_theory_btn.setEnabled(enable_clear_theory)
[docs]class InputTabPka(InputTabBase): """ An input tab that allows the user to specify pKa atoms. :ivar set_pka_marker: A signal emitted when a new pKa atom should be marked in the workspace. Emitted with two arguments: - The entry id of the structure to be marked (str) - The atom to be marked (`schrodinger.structure._StructureAtom`) :vartype set_pka_marker: `PyQt5.QtCore.pyqtSignal` :ivar remove_all_pka_markers: A signal emitted when the panel is reset and should thus remove all markers from the workspace. :vartype remove_all_pka_markers: PyQt5.QtCore.pyqtSignal """ HELP_TOPIC = "JAGUAR_INPUT_TAB" UI_MODULES = (ui.input_pka_top_ui, ui.input_pka_bottom_ui) MODEL_CLASS = input_tab_widgets_pka.InputEntriesModelPka VIEW_CLASS = input_tab_widgets_pka.InputEntriesViewPka set_pka_marker = QtCore.pyqtSignal(str, object) remove_all_pka_markers = QtCore.pyqtSignal() SMARTS = "SMARTS" SMARTS_TOOLTIP = "" AUTO = "Automatic search" AUTO_TOOLTIP = ("Application will attempt to locate all pKa atoms in all " "structures.\nStructures must be in the appropriate " "protonation form.\nNote that this search is not perfect " "and may miss some atoms or include others erroneously.")
[docs] def setup(self): super(InputTabPka, self).setup() self._input_entries_model.set_pka_marker.connect( self.set_pka_marker.emit) self.input_entries_view.set_pka_marker.connect(self.set_pka_marker.emit) # Set validators self.ui.conf_energy_win_le.setValidator(QtGui.QDoubleValidator()) self.ui.pick_atom_method_combo.insertItems(0, [self.AUTO, self.SMARTS]) self.ui.define_btn = input_tab_widgets_pka.SmartsSelectorButton(self) self.ui.define_btn.popUpClosing.connect(self.runSmartsSearch) self.hideDefineButton() self.ui.button_layout.addWidget(self.ui.define_btn) self._input_entries_model.projUpdated.connect(self.runSmartsSearch) self.ui.pick_atom_chk.toggled.connect(self.onFindCheckToggled) self.ui.pick_mode_frame.setToolTip(self.AUTO_TOOLTIP) self.ui.pick_atom_method_combo.currentTextChanged.connect( self.onPickModeComboChanged)
[docs] def onFindCheckToggled(self, state): """ If checked, disable editing of pka atom and enable combobox for choosing of pick mode. Update model to reflect new pick mode. Show smarts selector if new picking mode is SMARTS. :param state: If checkbox is checked or not :type state: bool """ self.ui.pick_atom_method_combo.setEnabled(state) self.input_entries_view.setEditablePkaAtomDelegate(not state) self.onPickModeChanged()
[docs] def onPickModeComboChanged(self): """ Update model to reflect new pick mode. Show smarts selector if new picking mode is SMARTS. """ self.onPickModeChanged()
[docs] def onPickModeChanged(self): """ Update the model to reflect the new pick mode. If its SMARTS show and hide the define label and connect signals. """ mode = self.getPickingMode() self._input_entries_model.setPickingMode(mode) if mode == input_tab_widgets_pka.PickingModes.SMARTS: self.ui.pick_mode_frame.setToolTip(self.SMARTS_TOOLTIP) self.showDefineButton() self.ui.define_btn.showSmartsSelector() else: self.ui.pick_mode_frame.setToolTip(self.AUTO_TOOLTIP) self.hideDefineButton()
[docs] def showDefineButton(self): """ Show the define label """ self.ui.define_btn.setVisible(True) self.ui.define_btn.setEnabled(True)
[docs] def hideDefineButton(self): """ Hide the define label """ self.ui.define_btn.setVisible(False) self.ui.define_btn.setEnabled(False)
[docs] def getPickingMode(self): """ Determine the current picking mode the tab is in :return: picking mode :rtype: input_tab_widgets_pka.PickingModes """ modes = input_tab_widgets_pka.PickingModes mode = modes.MANUAL if self.ui.pick_atom_chk.isChecked(): if self.ui.pick_atom_method_combo.currentText() == self.SMARTS: mode = modes.SMARTS elif self.ui.pick_atom_method_combo.currentText() == self.AUTO: mode = modes.AUTO return mode
[docs] def runSmartsSearch(self): """ Run SMARTS searches on the entries in the entry table if the pick mode is SMARTS """ if self.getPickingMode() == input_tab_widgets_pka.PickingModes.SMARTS: models = self.ui.define_btn.getModels() self._input_entries_model.updatePkaAtomsFromSmarts(models)
[docs] def getMmJagKeywords(self): keywords = super(InputTabPka, self).getMmJagKeywords() if self.getPickingMode() is input_tab_widgets_pka.PickingModes.AUTO: keywords[mm.MMJAG_SKEY_IPKASEARCH] = "all" else: keywords[mm.MMJAG_SKEY_IPKASEARCH] = None keywords[mm.MMJAG_SKEY_IPKAT] = None keywords[mm.MMJAG_SKEY_IPKASITES] = None # Get conformation search options. keywords[mm.MMJAG_IKEY_IPKA_CSRCH_ACC] = None keywords[mm.MMJAG_IKEY_IPKA_MAX_CONF] = None keywords[mm.MMJAG_RKEY_IPKA_ERG_WINDOW] = None if self.ui.perform_search_frame.isEnabled(): # conformation search accuracy options ipka_csrch_acc = mm.MMJAG_IPKA_CSRCH_ACC_LOW if self.ui.accuracy_thorough_rb.isChecked(): ipka_csrch_acc = mm.MMJAG_IPKA_CSRCH_ACC_HIGHER keywords[mm.MMJAG_IKEY_IPKA_CSRCH_ACC] = ipka_csrch_acc # maximum number of conformers to use option ipka_max_conf = self.ui.max_num_conf_sb.value() keywords[mm.MMJAG_IKEY_IPKA_MAX_CONF] = ipka_max_conf # energy window in kcal/mol option ipka_erg_window = float(self.ui.conf_energy_win_le.text()) keywords[mm.MMJAG_RKEY_IPKA_ERG_WINDOW] = ipka_erg_window return keywords
def _getKeywordsForStruc(self, cur_struc): mode_is_not_auto = (self.getPickingMode() is not input_tab_widgets_pka.PickingModes.AUTO) if mode_is_not_auto and cur_struc.pka_atom: pka_atoms = ",".join([str(x) for x in cur_struc.pka_atom]) else: pka_atoms = None return { mm.MMJAG_IKEY_MOLCHG: cur_struc.charge, mm.MMJAG_IKEY_MULTIP: cur_struc.spin_mult, mm.MMJAG_SKEY_IPKASITES: pka_atoms }
[docs] def isZwitterionChecked(self): return self.ui.zwitterion_cb.isChecked()
[docs] def isConformationalSearchesChecked(self): return self.ui.conf_searches_cb.isChecked()
[docs] def validate(self): retval = super(InputTabPka, self).validate() if retval: return retval if self.getPickingMode() is input_tab_widgets_pka.PickingModes.AUTO: return bad_atoms, missing_atoms = self._input_entries_model.checkPkaAtoms() if bad_atoms: plural_s = "s" if len(bad_atoms) > 1 else "" struc_list = ", ".join(bad_atoms) return ("Invalid pKa atom selected for structure%s %s." % (plural_s, struc_list)) elif missing_atoms: plural_s = "s" if len(missing_atoms) > 1 else "" struc_list = ", ".join(missing_atoms) return ("No pKa atom selected for structure%s %s." % (plural_s, struc_list))
[docs] def loadSettings(self, jag_input): super(InputTabPka, self).loadSettings(jag_input) # TODO!!! Load conformation search option. This # can be done when all options set by this panel are stored # in the input file using mmjag keywords rather than command # line arguments. # Load conformation search accuracy options. ipka_csrch_acc = jag_input[mm.MMJAG_IKEY_IPKA_CSRCH_ACC] if ipka_csrch_acc == mm.MMJAG_IPKA_CSRCH_ACC_LOW: self.ui.accuracy_fast_rb.setChecked(True) else: self.ui.accuracy_thorough_rb.setChecked(True) # Load maximum number of conformers to use ipka_max_conf = jag_input[mm.MMJAG_IKEY_IPKA_MAX_CONF] self.ui.max_num_conf_sb.setValue(ipka_max_conf) # Load energy window in kcal/mol ipka_erg_window = jag_input[mm.MMJAG_RKEY_IPKA_ERG_WINDOW] self.ui.conf_energy_win_le.setText(str(ipka_erg_window)) self._loadPkaAtom(jag_input)
def _loadPkaAtom(self, jag_input): """ Load the pKa atom from the mmjag keywords. Note that the setting will be ignored if it is set to the default (i.e. no pKa atom name). If the setting is non-default, then a warning will be issued unless there is exactly one structure loaded into the table. :param jag_input: A JaguarInput object containing the settings to load :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` """ atoms = self._getPkaAtomFromJagInput(jag_input) model = self._input_entries_model if atoms: if not self._checkSingleStructure("pKa atom"): return self.setPickMethodToManual() pka_index_add = model.index(0, model.COLUMN.PKA_ATOM_ADD) pka_index_remove = model.index(0, model.COLUMN.PKA_ATOM_REMOVE) # setData() filters atoms to ensure appropriate ones go into each column model.setData(pka_index_add, atoms) model.setData(pka_index_remove, atoms) if jag_input[mm.MMJAG_SKEY_IPKASEARCH] == "all": if atoms: msg = ("Custom pKa sites were defined but ipkasearch=all was " "set. To use an automatic search, select 'Automatic " "Search' from the dropdown.") warnings.warn(JaguarSettingWarning(msg)) else: self.setPickMethodToAuto() def _getPkaAtomFromJagInput(self, jag_input): """ Get one or more pka atoms from a jaguar input :param jag_input: The Jaguar Input object :type jag_input: schrodinger.application.jaguar.input.JaguarInput :return: A list of Jaguar atom names :rtype: list(str) """ atoms = [] pka_atom = jag_input[mm.MMJAG_SKEY_IPKAT] if pka_atom: atoms.append(pka_atom) pka_atoms = jag_input[mm.MMJAG_SKEY_IPKASITES] if pka_atoms: atoms.extend(pka_atoms.split(",")) return atoms
[docs] def setPickMethodToManual(self): self.ui.pick_atom_chk.setChecked(False)
[docs] def setPickMethodToAuto(self): self.ui.pick_atom_chk.setChecked(True) self.ui.pick_atom_method_combo.setCurrentText(self.AUTO)
[docs] def reset(self): super().reset() self.remove_all_pka_markers.emit()
[docs]class InputTabHB(InputTabBase): """ An input tab that is used with Hydrogen Bond task. """ HELP_TOPIC = "JAGUAR_HYDROGEN_BOND" UI_MODULES = (ui.input_hb_top_ui, ui.input_hb_bottom_ui) MODEL_CLASS = input_tab_widgets_hb.InputEntriesModelHB VIEW_CLASS = input_tab_widgets.InputEntriesView
[docs] def getMmJagKeywords(self): """ There are no jaguar handle keywords that this tab sets, but we need this function to override the one in the base class. """ keywords = {} return keywords
def _getKeywordsForStruc(self, cur_struc): keywords = { mm.MMJAG_IKEY_MOLCHG: cur_struc.charge, mm.MMJAG_IKEY_MULTIP: cur_struc.spin_mult } return keywords
[docs] def loadSettings(self, jag_input): self.reset() super().loadSettings(jag_input)
[docs] def isFastMode(self): """ This function return True if 'Fast Mode' check box is checked. :return: True if 'Fast Mode' check box is toggled. :rtype: bool """ return self.ui.fast_mode_cb.isChecked()
[docs] def getOptimizationCmd(self): """ This function returns command argument corresponding to the checked optimization option. If 'Optimize all structure' option is selected this function returns None. :return: optimization command argument :rtype: string or None """ if self.ui.opt_individual_rb.isChecked(): return "-noopt_complex" elif self.ui.opt_none_rb.isChecked(): return "-noopt" elif self.ui.opt_freeze_rb.isChecked(): return "-noopt_torsions" return None
[docs] def reset(self): """ This function resets optimization and 'fast mode' settings to default state. This function needs to be called every time when input file is loaded. """ self.ui.fast_mode_cb.setChecked(False) self.ui.opt_all_rb.setChecked(True)
# TODO: modify inheritance so any type of input tab can use a sub tab (hide # sub_tab_widget if INPUT_SUB_TABS is Falsey?)
[docs]class InputTabWithSubTabs(InputTab): """ An Input tab with sub-tabs on the bottom. Sub-tabs can be set using `addSubTabs`, which is normally accessed by setting the INPUT_SUB_TABS panel class variable. :cvar DEFAULT_SUB_TAB_VISIBILITY: Whether the sub-tabs should be visible by default. Sub-tab visibility can be toggled at any time using `setSubTabsVisible`, which is connected to the "Atom-Level Settings" button. :vartype DEFAULT_SUB_TAB_VISIBILITY: bool :ivar subTabChanged: A signal emitted when the active sub-tab has been changed, or when the sub-tab visibility has been toggled. :vartype subTabChanged: `PyQt5.QtCore.pyqtSignal` :ivar addJaguarMarker: A signal emitted when a workspace marker should be added. Emitted with: - The list of atoms to add the marker for (list) - The marker settings (dict) - The name of the sub-tab that the marker is for (str) :vartype addJaguarMarker: `PyQt5.QtCore.pyqtSignal` :ivar removeJaguarMarker: A signal emitted when a workspace marker should be removed. Emitted with: - The list of atoms to remove the marker for (list) - The name of the sub-tab that the marker is for (str) :vartype removeJaguarMarker: `PyQt5.QtCore.pyqtSignal` :ivar setMarkerHighlighting: A signal emitted when the highlighting of a workspace marker should be changed. Emitted with: - The list of atoms to change the highlighting for (list) - Whether the marker should be highlighted (True) or unhighlighted (False) (bool) - The name of the sub-tab that the marker is for (str) :vartype setMarkerHighlighting: `PyQt5.QtCore.pyqtSignal` """ UI_MODULES = (ui.input_top_ui, ui.input_std_bottom_ui) DEFAULT_SUB_TAB_VISIBILITY = False SUB_TAB_NAME = "%s (%i)" subTabChanged = QtCore.pyqtSignal() addJaguarMarker = QtCore.pyqtSignal(list, dict, str) removeJaguarMarker = QtCore.pyqtSignal(list, str) setMarkerHighlighting = QtCore.pyqtSignal(list, bool, str) def _populateUi(self): """ After combining all the ui files, create: - A QPushButton to toggle the visibility of the sub-tabs (self.ui.show_sub_tabs_btn) - A QSplitter to adjust the relative sizes of the input table and the sub tabs (self.ui.spliiter) - A QTabWidget to show the sub tabs (self.ui.sub_tab_widget) These widgets are added here rather than via a ui file because the splitter must contain all of the input tab contents in order to function properly, and that's not possible with the current ui-combining mechanism. """ super()._populateUi() main_layout = self.layout() show_sub_tabs_btn = QtWidgets.QPushButton() self.ui.show_sub_tabs_btn = show_sub_tabs_btn show_sub_tabs_btn.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) main_layout.addWidget(show_sub_tabs_btn) splitter = QtWidgets.QSplitter(Qt.Vertical, None) self.ui.splitter = splitter upper_widget = QtWidgets.QWidget(splitter) upper_widget.setLayout(main_layout) self.ui.sub_tab_widget = QtWidgets.QTabWidget(splitter) # This tab bar must have a different name than the panel tab bar. # Otherwise, Squish can't tell them apart. (See PANEL-3888.) self.ui.sub_tab_widget.tabBar().setObjectName("InputSubTabTabBar") new_main_layout = QtWidgets.QVBoxLayout(self) new_main_layout.addWidget(splitter) splitter.setCollapsible(0, False)
[docs] def addSubTabs(self, sub_tab_classes): """ Add a sub tab for each of the specified classes. Note that this function should be called only once per instance. :param sub_tab_classes: An iterable of `schrodinger.application.jaguar. gui.tabs.input_sub_tabs.base_sub_tab.BaseSubTab` sub-classes to be instantiated. :type sub_tab_classes: iterable """ self.sub_tabs = [cur_sub_tab(self) for cur_sub_tab in sub_tab_classes] with qt_utils.suppress_signals(self.ui.sub_tab_widget): # Prevent the tabChanged signal from being emitted when the first # tab is added, since that will prematurely activate the picking # check box for cur_sub_tab in self.sub_tabs: name = self.SUB_TAB_NAME % (cur_sub_tab.NAME, 0) self.ui.sub_tab_widget.addTab(cur_sub_tab, name) cur_sub_tab.countChanged.connect(self._subTabCountChanged) cur_sub_tab.addJaguarMarker.connect(self.addJaguarMarker.emit) cur_sub_tab.removeJaguarMarker.connect( self.removeJaguarMarker.emit) cur_sub_tab.setMarkerHighlighting.connect( self.setMarkerHighlighting.emit) self._configurePerAtomBasis()
def _configurePerAtomBasis(self): """ If there is a basis set sub-tab, connect the per-atom basis set model to the input table model. This allows the input table to use the per-atom basis sets when determining basis set validity and basis function counts. """ basis_sub_tab = self.getBasisSetSubTab() if basis_sub_tab: per_atom_basis_model = basis_sub_tab._table_model self._input_entries_model.setPerAtomBasisModel(per_atom_basis_model)
[docs] def getChargeConstraintSubTab(self): """ Get the subtab that holds the charge constraint settings :rtype: `schrodinger.application.jaguar.gui.input_sub_tabs. ChargeConstraintsSubTab` :return: The charge constraint subtab, or None if no such tab exists """ try: return self.getSubTab(input_sub_tabs.ChargeConstraintsSubTab) except ValueError: return None
[docs] def getBasisSetSubTab(self): """ Get the subtab that holds the by-atom basis set settings :rtype: `schrodinger.application.jaguar.gui.input_sub_tabs. BasisSetSubTab` :return: The by-atom basis set subtab, or None if no such tab exists """ try: return self.getSubTab(input_sub_tabs.BasisSetSubTab) except ValueError: return None
[docs] def getSubTab(self, sub_tab_class): """ Get the sub-tab of the specified class :param sub_tab_class: The class of the sub-tab to retrieve :type sub_tab_class: type """ tabs = [tab for tab in self.sub_tabs if isinstance(tab, sub_tab_class)] if not tabs: raise ValueError("No sub-tab found") elif len(tabs) > 1: raise ValueError("Multiple sub-tabs found") else: return tabs[0]
[docs] def setup(self): super().setup() self.setSubTabsVisible(self.DEFAULT_SUB_TAB_VISIBILITY) self.ui.show_sub_tabs_btn.clicked.connect( lambda: self.setSubTabsVisible()) self.ui.sub_tab_widget.currentChanged.connect(self._subTabChanged) self.ui.splitter.splitterMoved.connect(self._splitterMoved) self._prev_sub_tab_size = None self._prev_sub_tab_visibility = False
def _subTabCountChanged(self, sub_tab, count): """ Whenever the row count changes in one of the sub tab tables, update the sub tab title with the new count. :param sub_tab: The sub tab to update the title of :type sub_tab: `schrodinger.application.jaguar.gui.tabs.input_sub_tabs. base_sub_tab.BaseSubTab` :param count: The new row count :type count: int """ sub_tab_widget = self.ui.sub_tab_widget index = sub_tab_widget.indexOf(sub_tab) name = self.SUB_TAB_NAME % (sub_tab.NAME, count) sub_tab_widget.setTabText(index, name)
[docs] def projectUpdated(self): """ Whenever the project is updated, update the structure titles and the structures shown in the sub tabs. """ super().projectUpdated() eids_to_titles = self._input_entries_model.entryTitles() eids = list(eids_to_titles) for cur_sub_tab in self.sub_tabs: cur_sub_tab.setDisplayedEids(eids) cur_sub_tab.updateEntryTitles(eids_to_titles)
[docs] def reset(self): super(InputTabWithSubTabs, self).reset() for cur_sub_tab in self.sub_tabs: cur_sub_tab.reset() sub_tab_index = self.ui.sub_tab_widget.currentIndex() self._subTabChanged(sub_tab_index)
[docs] def setSubTabsVisible(self, visible=None): """ Set whether the sub-tabs are visible in response to the user clicking on the "Atom-Level Settings" button. :param visible: Should the sub-tabs be made visible. If not specified, the visibility will be toggled. :type visible: bool or NoneType """ splitter = self.ui.splitter upper_size, lower_size = splitter.sizes() if visible is None: visible = not lower_size if visible: if self._prev_sub_tab_size is None: new_lower_size = self.ui.sub_tab_widget.sizeHint().height() else: new_lower_size = self._prev_sub_tab_size else: self._prev_sub_tab_size = lower_size new_lower_size = 0 splitter.setSizes([upper_size - new_lower_size, new_lower_size]) self._updateAfterSubTabsVisible(visible)
def _updateAfterSubTabsVisible(self, visible): """ Update the panel after sub-tab visibility was changed :param visible: Were the sub-tabs made visible? :type visible: bool """ self._prev_sub_tab_visibility = visible btn_suffix = "<<" if visible else ">>" text = "Atom-Level Settings " + btn_suffix self.ui.show_sub_tabs_btn.setText(text) widget = self.ui.sub_tab_widget cur_sub_tab = widget.currentWidget() if cur_sub_tab is not None: widget.currentWidget().setActive(visible) self.subTabChanged.emit()
[docs] def subTabsVisible(self): """ Are the sub-tabs currently visible? :return: True if the sub-tabs are visible. False otherwise. :rtype: bool """ upper_size, lower_size = self.ui.splitter.sizes() return bool(lower_size)
def _splitterMoved(self): """ If the user just toggled sub-tab visibility by dragging the slider, update the panel appropriately """ visible = self.subTabsVisible() if visible != self._prev_sub_tab_visibility: self._updateAfterSubTabsVisible(visible) def _subTabChanged(self, new_index): """ When the sub-tab is changed, activate the newly selected sub-tab :param new_index: The index of the newly selected sub-tab :type new_index: int """ for cur_sub_tab in self.sub_tabs: cur_sub_tab.deactivate() self.sub_tabs[new_index].activate() self.subTabChanged.emit()
[docs] def validate(self): # See parent class for documentation err = super(InputTabWithSubTabs, self).validate() if err: return err for index, cur_sub_tab in enumerate(self.sub_tabs): if not self.ui.sub_tab_widget.isTabEnabled(index): # Don't bother to validate disabled tabs since those settings # won't be saved continue err = cur_sub_tab.validate() if err: self.ui.sub_tab_widget.setCurrentWidget(cur_sub_tab) return err
[docs] def saveSettings(self, jag_input, eid): # See parent class for documentation super(InputTabWithSubTabs, self).saveSettings(jag_input, eid) for index, cur_sub_tab in enumerate(self.sub_tabs): # Don't save settings from disabled tabs if self.ui.sub_tab_widget.isTabEnabled(index): cur_sub_tab.saveSettings(jag_input, eid)
[docs] def loadPerAtomSettings(self, jag_input, eid, title): # See parent class for documentation super(InputTabWithSubTabs, self).loadSettings(jag_input) struc = jag_input.getStructure() for cur_sub_tab in self.sub_tabs: # We *should* try to load settings into disabled tabs so we can warn # the user if there are any settings that will be ignored cur_sub_tab.loadSettings(jag_input, eid, title, struc)
[docs] def activeSubTab(self): """ Get the active sub tab :return: The active sub-tab or None if the sub-tabs are not visible :rtype: `schrodinger.application.jaguar.gui.input_sub_tabs. base_sub_tab.BaseSubTab` or NoneType """ if not self.subTabsVisible(): return None else: return self.ui.sub_tab_widget.currentWidget()
[docs] def activeSubTabName(self): """ Get the name of the active sub-tab :return: The name of the active sub-tab or None if the sub-tabs are not visible :rtype: str or NoneType """ sub_tab = self.activeSubTab() if sub_tab is not None: sub_tab = sub_tab.NAME return sub_tab
[docs] def activate(self): """ Activate the appropriate sub-tab (if any) when this tab is activated """ sub_tab = self.activeSubTab() if sub_tab is not None: sub_tab.activate()
[docs] def deactivate(self): """ Deactivate the active sub-tab (if any) """ sub_tab = self.activeSubTab() if sub_tab is not None: sub_tab.deactivate()
[docs] def displayedEntryIds(self): """ Return the entry IDs for all structures that are currently displayed in the input table :return: A set of entry ids :rtype: set """ return self._input_entries_model.entryIds()
[docs] def perAtomBasisSetsPresent(self): """ Have any per-atom basis sets been set? :return: True if there are per-atom basis sets for any currently selected structures. False otherwise. :rtype: bool """ basis_sub_tab = self.getBasisSetSubTab() if basis_sub_tab: return basis_sub_tab.perAtomBasisSetsPresent() else: return False
[docs] def getCommonBasis(self): """ If all structures use the same basis set and there are no per-atom basis sets, return the basis set name. Otherwise, return None. :return: The basis set name or None :rtype: str or NoneType """ if not self.perAtomBasisSetsPresent(): return super(InputTabWithSubTabs, self).getCommonBasis() else: return None
[docs] def theoryChanged(self, theory_level): """ If there is a Charge Constraints sub-tab, disable or enable it when the level of theory changes. :param theory_level: The current level of theory. Should be one of "DFT", "HF", "LMP2", "RIMP2" or "Mixed". :type theory_level: str """ charge_sub_tab = self.getChargeConstraintSubTab() if not charge_sub_tab: return widget = self.ui.sub_tab_widget charge_index = widget.indexOf(charge_sub_tab) enable = theory_level == gui_utils.THEORY_DFT if enable: tool_tip = "" else: tool_tip = ("Charge constraints are only available for the DFT " "level of theory.") widget.setTabEnabled(charge_index, enable) widget.setTabToolTip(charge_index, tool_tip)
[docs] def useFromComboChanged(self): """ Update the sub-tabs when the user changes the structure source (from included to selected or vice versa). """ super().useFromComboChanged() eids = self._input_entries_model.entryIds() for cur_sub_tab in self.sub_tabs: cur_sub_tab.setDisplayedEids(eids)
[docs]class InputTabCoordinateScan(InputTab): """ Input tab used in Rigid and Relaxed Coordinate scan GUIs. It modifies 'Input structures from' combo box to include only the 'Workspace (included)' item. """
[docs] def setup(self): super().setup() self.ui.use_from_combo.removeSelectedEntriesItem()