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

import copy
import os
import re
import warnings

import schrodinger
from schrodinger import project
from schrodinger import structure
from schrodinger.application.jaguar import basis as jag_basis
from schrodinger.application.jaguar import input as jaginput
from schrodinger.application.jaguar import user_config
from schrodinger.application.jaguar.input import JAGUAR_EXE
from schrodinger.application.jaguar.input import JaguarInput
from schrodinger.infra import mm
from schrodinger.infra import mmcheck
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import appframework as af1
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt.appframework2 import af2
from schrodinger.utils import fileutils

from . import tabs
from . import utils as gui_utils
from .edit_dialog import EditDialog
from .filedialog import ReadFileDialog
from .ui import config_dialog_jaguar_ui
from .ui import coordinate_dialog_ui
from .utils import MOLECULAR_CHARGE_PROP
from .utils import SPIN_MULT_PROP
from .utils import JaguarSettingError

maestro = schrodinger.get_maestro()

CONSTRAINTS_ERR = ("Only one entry may be selected in the project table "
                   "when adding constraints")


[docs]class JaguarConfigDialog(af2.ConfigDialog): """ Just like ConfigDialog except that new UX is used for OpenMP settings: Instead of option to select threads vs CPUs, now there is a checkbox for limiting the number of subjobs, and a #max subjobs spinbox. """ def _setupOpenMPWidgets(self): """ Add all the widgets to the dialog to allow the user the option of specifying the number of processors and simultaneous subjobs. """ self.open_mp_widget = QtWidgets.QWidget() self.open_mp_ui = config_dialog_jaguar_ui.Ui_Form() self.open_mp_ui.setupUi(self.open_mp_widget) self.open_mp_ui.open_mp_cpu_layout.addWidget(self.num_cpus_sb) self.open_mp_ui.open_mp_cpu_layout.addStretch() self.job_layout.addWidget(self.open_mp_widget) self.open_mp_ui.mp_limit_subjobs_cb.setToolTip( "Each simultaneous subjob will require its own license tokens.\n" + "Limit the number of simultaneous subjobs to reduce license usage.") self.open_mp_ui.mp_limit_subjobs_cb.toggled.connect( self.updateOpenMPInfo) self.open_mp_ui.open_mp_cpu_layout.addWidget(self.num_cpus_sb) self.num_cpus_sb.valueChanged.connect( self.open_mp_ui.mp_max_subjobs_sb.setMaximum) self.open_mp_ui.mp_max_subjobs_sb.setFixedWidth( af2.config_dialog.FIXED_SB_WIDTH) self._loadFromPrefs() def _loadFromPrefs(self): """ Load the #cpus and #max subjobs from preferences. """ cpu_key = self.last_open_mp_total_cpus_prefkey pref_processes = self._app_preference_handler.get(cpu_key, 1) subjobs_key = self.last_open_mp_subjobs_prefkey pref_subjobs = self._app_preference_handler.get(subjobs_key, 0) self.num_cpus_sb.setValue(pref_processes) self.open_mp_ui.mp_max_subjobs_sb.setValue(pref_subjobs) self.open_mp_ui.mp_limit_subjobs_cb.setChecked(pref_subjobs) self.updateOpenMPInfo()
[docs] def updateOpenMPInfo(self): """ Enable/disable the limit subjobs widgets based on whether the checkbox is checked or not. """ limit_subjobs = self.open_mp_ui.mp_limit_subjobs_cb.isChecked() self.open_mp_ui.max_label.setEnabled(limit_subjobs) self.open_mp_ui.mp_max_subjobs_sb.setEnabled(limit_subjobs) if limit_subjobs: tip = ("If not limited, subjobs may run simultaneously on each of" " the processors.\nEach simultaneous subjob will require" " its own license tokens.") else: tip = ("Each simultaneous subjob will require its own license " " tokens.\nLimit the number of simultaneous subjobs to " " reduce license usage.") self.open_mp_ui.info_btn.setToolTip(tip)
[docs] def getNumCpusToValidate(self, is_queue=False): """ Return the max # CPUs to pass in to validateNumOpenMP() :is_queue: Ignored :is_queue: bool :rtype: int :return: total number of CPUs """ return self.num_cpus_sb.value()
[docs] def getOpenMPSettings(self): """ Based on Open MP settings, return a tuple of: * Maximum number of CPUs to use * Number of threads to use (always 0 in this subclass) * Maximum number of subjobs to create. :return: (#cpus, #threads, #subjobs) :rtype: (int, int, int) """ openmpcpus = self.num_cpus_sb.value() threads = 0 if not self.open_mp_ui.mp_limit_subjobs_cb.isChecked(): # User did not break down the number of threads/subjobs openmpsubjobs = 0 else: openmpsubjobs = self.open_mp_ui.mp_max_subjobs_sb.value() return openmpcpus, threads, openmpsubjobs
def _formJaguarCPUFlags(self, use_parallel_flag=True): """ Determine the command line flags for an Open MP job. Over-rides base method to only use -HOST and -PARALLEL options for specifying parallelization options, without use of -TPP. :param use_parallel_flag: Ignored. :type use_parallel_flag: bool :return: The appropriate command line flags. :rtype: list """ cd_params = self.getSettings() host = cd_params.host cpus = cd_params.openmpcpus subjobs = cd_params.openmpsubjobs # Whether limiting subjobs or not: host_str = '%s:%s' % (host, subjobs) if subjobs != 0 else host return ['-HOST', host_str, '-PARALLEL', str(cpus)]
[docs]class BaseJaguarPanel(af2.JobApp): # This class must *not* be named "BasePanel". That causes Squish to crash. # (QA-1355) """ A base class for all Jaguar GUIs. Subclasses should define TABS and TASK_NAME. :cvar TASK_NAME: The name of the panel :vartype TASK_NAME: str :cvar SHORT_NAME: A shortened version of `TASK_NAME`. Used in constructing job names. :vartype SHORT_NAME: str :cvar TABS: The list of tabs that a panel should contain. Each tab must be a `schrodinger.application.jaguar.gui.tabs.base_tab.BaseTab` subclass. :vartype TABS: list :cvar INPUT_SUB_TABS: The list of sub-tabs for the Input tab. Only used if `TABS` contains a subclass of `schrodinger.application.jaguar.gui.tabs. input_tab.InputTabWithSubTabs`. Each sub-tab must be a `schrodinger. application.jaguar.gui.tabs.input_sub_tabs.base_sub_tab.BaseSubTab` subclass. :vartype INPUT_SUB_TABS: list :cvar EDIT_DLG_CLASS: The edit dialog class to use for the panel. Subclasses may override this to be a subclass of `EditDialog`. """ TASK_NAME = "" SHORT_NAME = "" TABS = [] INPUT_SUB_TABS = [] EDIT_DLG_CLASS = EditDialog
[docs] def getTabParentAndLayout(self): """ Get the parent widget for all tabs and layout to place the tabs into. """ # This method allows us to place the tabs either into this BasePanel # directly (for Jaguar GUIs) or overwrite it to place them into a frame # in the Jaguar Multistage panel return self, self.main_layout
[docs] def setPanelOptions(self): """ Define instance variables that specify the creation of the panel. """ super(BaseJaguarPanel, self).setPanelOptions() self.title = "Jaguar - %s" % self.TASK_NAME filetypes = [("Jaguar Structure Input", "*.in *.mae *.maegz *.mae.gz")] self.input_selector_options = { "filetypes": filetypes, "included_entries": True, "file_text": "Files:", "multiplefiles": True, "default_source": "included_entries", "tracking": True } self.program_name = 'Jaguar' self.default_jobname = 'jaguar' self.help_topic = True
# We've defined a custom help function, but help_topic must be true # to get the help button to appear
[docs] def setup(self): """ Instantiate the tab widget and tabs. Sub-tabs will also be added to the Input tab if necessary. """ super(BaseJaguarPanel, self).setup() tab_parent, tab_layout = self.getTabParentAndLayout() self.tab_widget = QtWidgets.QTabWidget(self) # This tab bar must have a different name than the Input tab sub-tab # selector tab bar. Otherwise, Squish can't tell them apart. (See # PANEL-3888.) self.tab_widget.tabBar().setObjectName("JaguarTabBar") self.tabs = [ cur_tab(tab_parent, self.input_selector) for cur_tab in self.TABS ] for cur_tab in self.tabs: self.tab_widget.addTab(cur_tab, cur_tab.NAME) if len(self.tabs) == 1: self.tab_widget.tabBar().hide() tab_layout.addWidget(self.tab_widget) try: input_tab = self.getTab(tabs.InputTabWithSubTabs) except ValueError: pass else: input_tab.addSubTabs(self.INPUT_SUB_TABS) self.keywords_le = swidgets.SLabeledEdit('Keywords:', layout=tab_layout, stretch=False) self.keywords_le.setToolTip( 'Specify keywords and macros for the gen section, which will ' 'override settings made elsewhere in the GUI.') jag_input = self._createDefaultJagInput() self._loadSettingsWithErrors(jag_input) self.edit_dialog = None
def _createDefaultJagInput(self): """ Create a JaguarInput object containing default keywords taken from all tabs :return: A JaguarInput object containing default keywords :rtype: `schrodinger.application.jaguar.input.JaguarInput` :note: An "empty" JaguarInput object contains all mmjag defaults, so most tabs will not return any default keywords. Mmjag defaults do not specify a theory level or a DFT functional, however, and the panel expects defaults for these. """ defaults = {} for cur_tab in self.tabs: cur_tab_defaults = cur_tab.getDefaultKeywords() defaults.update(cur_tab_defaults) jag_input = JaguarInput(genkeys=defaults) return jag_input
[docs] def getTab(self, tab_class): """ Get the tab of the specified class :param tab_class: The class of the tab to retrieve :type tab_class: type """ tabs = [tab for tab in self.tabs if isinstance(tab, tab_class)] if not tabs: raise ValueError("No tab found") elif len(tabs) > 1: raise ValueError("Multiple tabs found") else: return tabs[0]
def _help(self): """ Display help for the current tab """ help_topic = self.tab_widget.currentWidget().HELP_TOPIC af1.help_dialog(help_topic, parent=self) def _parseKeywordsLE(self): """ Return a dictionary of keywords set by the sequence of statements in the Keywords line editor, which may consist of keyword[ ]=[ ]value and macro names, separated by spaces. :param text: text to be converted to a dictionary :type text: str :return: dictionary :rtype: dict :raise ValueError: if a macro is not defined :raise JaguarConfigError: if there are issues with the macros in the jaguar configuration file """ text = self.keywords_le.getString() statements = re.sub(r'\s*=\s*', '=', text).split() keywords = {} for statement in statements: if "=" in statement: k, v = statement.split("=", 1) if v.endswith(','): v = v.rstrip(',') keywords[k] = v else: keywords.update(user_config.get_macro(statement)) return keywords
[docs] def launchJaguarJob(self, jag_input): """ Launch the specified job. :param jag_input: A JaguarInput object to run :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` :return: A job object for the launched job :rtype: `schrodinger.job.jobcontrol.Job` """ infile = "%s.in" % self.jobname() # do not modify the jag_input itself save_jag_input = copy.copy(jag_input) # copy the _structures dict in order to generate the needed .mae files save_jag_input._structures = self._jag_input._structures save_jag_input.setValues(self._parseKeywordsLE()) save_jag_input.saveAs(infile, follow_links=mm.MMJAG_APPEND_X_OVERWRITE, validate=True) cmd = self.createCmd() cmd.extend(self.jobnameArg()) cmd.append(infile) return self.launchJobCmd(cmd)
[docs] def createCmd(self): """ Create the command line for job launching. Note that the input file name will be added in `launchJaguarJob`. :note: This function is intended to be overridden in BaseJaguarPanel subclassses that use different command lines. This code should not be moved into launchJaguarJob. """ return [JAGUAR_EXE, "run"]
[docs] def jobnameArg(self): """ Build the command line arguments for passing the job name. :return: A list of command line arguments :rtype: list """ return ["-jobname=%s" % self.jobname()]
[docs] def getNumStructures(self): """ Get the number of structures that this panel is currently representing """ return gui_utils.count_num_strucs(self.input_selector)
[docs] @af2.appmethods.reset("Reset Panel") def reset(self): """ Reset the panel after prompting the user for confirmation :return: Returns False if the user does not want to reset. This will prevent AF2 from resetting the input selector. :rtype: bool or NoneType """ if self._resetWarning(): jag_input = self._createDefaultJagInput() self._loadSettingsWithErrors(jag_input) for cur_tab in self.tabs: cur_tab.reset() else: return False
def _loadSettings(self, jag_input, eid=None, title=None): """ Load the specified settings into the tabs and store them in self._jag_input. This function should not be called directly. Instead, _loadSettingsWithErrors(), _loadSettingsWithEditDialogWarnings(), or _loadSettingsWithReadWarnings() should be used so warnings are handled properly. :param jag_input: The Jaguar settings to load :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` :param eid: The entry id of the structure in `jag_input`. Only necessary if `jag_input` contains per-atom settings. :type eid: str :param title: The title of the structure in `jag_input`. Only necessary if `jag_input` contains per-atom settings. :type title: str """ self._jag_input = jag_input for cur_tab in self.tabs: cur_tab.loadSettings(jag_input) if eid: cur_tab.loadPerAtomSettings(jag_input, eid, title) def _loadSettingsWithErrors(self, jag_input, eid=None, title=None): """ Load the specified settings. If a warning is raised, it will immediately be raised as an exception. This function should be used when reading in default settings, since all default settings should be properly handled by the panel. See `_loadSettings` for argument documentation. :raise JaguarSettingWarning: If a tab cannot properly handle a specified setting """ with warnings.catch_warnings(): warnings.simplefilter("error", gui_utils.JaguarSettingWarning) self._loadSettings(jag_input, eid, title) def _resetWarning(self): """ Display a dialog to confirm that the user really wants to reset the panel :return: True if the user wants to reset. False otherwise. :rtype: bool """ msg_text = ("This action will reset all Jaguar options to their " "default values. Do you want to proceed?") msg_buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No retval = QtWidgets.QMessageBox.question(self, "Confirm Reset", msg_text, msg_buttons) return retval == QtWidgets.QMessageBox.Yes
[docs] def getConfigDialog(self): """ Get the job configuration dialog """ return JaguarConfigDialog(self, incorporation=True, allow_replace=True, default_disp=af2.DISP_APPEND, open_mp=True, host=True)
@af2.validator() def runTabValidation(self): """ Run validation for each tab. If any tab raises an error, immediately switch to that tab and return the error. """ for cur_tab in self.tabs: err = cur_tab.validate() if err: self.tab_widget.setCurrentWidget(cur_tab) return (False, err) return True @af2.validator() def checkKeywordsLE(self): try: self._parseKeywordsLE() except (ValueError, user_config.JaguarConfigError) as e: return False, str(e) return True
[docs] def generateStatus(self): """ Create the status bar message. This function will prepend any Jaguar- specific status bar message before the standard job information. The Jaguar-specific status bar message can be generated by defining generateJaguarStatus(). """ status = super(BaseJaguarPanel, self).generateStatus() jag_status = self.generateJaguarStatus() if jag_status: status = jag_status + ", " + status return status
[docs] def generateJaguarStatus(self): """ Create a Jaguar-specific status bar message. This function should be defined in any subclass that wishes to modify the status bar message. :return: The Jaguar-specific status bar message, or None if no such message is desired. :rtype: str or NoneType """
def _updateMmJag(self): """ Update the mmjag object that will be used to build the input file :raise JaguarSettingError: If any settings are invalid. """ self._addStructureToMmJag() self._getAllJagSettings()
[docs] def getMmJagKeywords(self): """ Get Jaguar keywords that are specific to this panel and not set in any of the tabs. :return: A dictionary of Jaguar keywords :rtype: dict """ keywords = {} keywords[mm.MMJAG_IKEY_IFLINKS] = mm.MMJAG_IFLINKS_ON return keywords
def _runPreflight(self): """ Run the mmjag preflight check, which checks settings for consistency. :raise JaguarSettingError: If any warnings were raised by the preflight check. """ preflight_err = self._jag_input.preflight() if preflight_err: raise JaguarSettingError(preflight_err) def _getAllJagSettings(self): """ Get the Jaguar keywords from all tabs and store them in the JaguarInput handle. Also get and store any task-specific keywords. :raise JaguarSettingError: If any settings are invalid. """ tab_settings = {} for cur_tab in self.tabs: cur_tab_settings = cur_tab.getMmJagKeywords() tab_settings.update(cur_tab_settings) tab_settings.update(self.getMmJagKeywords()) self._jag_input.setValues(tab_settings) def _getPerAtomSettings(self, eid): """ Load all per-atom settings into self._jag_input :param eid: The entry id to load settings for :type eid: str """ for cur_tab in self.tabs: cur_tab.saveSettings(self._jag_input, eid) def _clearPerAtomSettings(self): """ Remove all per-atom settings from the mmjag handle """ self._jag_input.clearAllConstraints() self._jag_input.clearChargeConstraints() self._jag_input.clearAtomicBases() def _clearFreezeProperties(self, struc): """ Delete all Maestro "freeze" atom properties, as these freezes didn't come from the panel but will be enforced as constraints by the Jaguar backend. (See PANEL-3518.) """ for atom in struc.atom: for prop_name in list(atom.property): if prop_name.startswith("b_m_freeze"): # The property name will be one of b_m_freeze_x, # b_m_freeze_y, or b_m_freeze_z del atom.property[prop_name] def _cleanStructureForJagInput(self, struct): """ Clean up a structure for use in Jaguar input, making sure it has proper atom names and removing any stale properties :type struct: `schrodinger.structure.Structure` :param struct: The structure to clean """ jaginput.apply_jaguar_atom_naming(struct) self._clearFreezeProperties(struct) def _addStructureToMmJag(self): """ Add the structure specified in the input selector frame to the mmjag handle. :raise JaguarSettingError: If the input selector is set to Files and the file can't be read. """ self._clearPerAtomSettings() try: struc = next(self.input_selector.structures(True)) self._cleanStructureForJagInput(struc) self._jag_input.setStructure(struc, mm.MMJAG_ZMAT1) except StopIteration: self._jag_input.deleteStructure(mm.MMJAG_ZMAT1) except IOError: err = "The specified structure file could not be read." raise JaguarSettingError(err) # Clear the other Z-matrices self._jag_input.deleteStructure(mm.MMJAG_ZMAT2) self._jag_input.deleteStructure(mm.MMJAG_ZMAT3)
[docs]class StandardStatusMixin(object): """ A mixin that will add the standard Jaguar status bar text to a Jaguar panel. Classes that use this mixin must have both a "Theory" tab and a "Molecule" tab with a basis selector. """
[docs] def __init__(self): """ Connect signals so the status bar is updated whenever the user changes the basis or theory level """ super(StandardStatusMixin, self).__init__() molecule_tab = self.getTab(tabs.ProvidesBasisMixin) molecule_tab.basis_changed.connect(self.updateStatusBar) method_tab = self.getTab(tabs.ProvidesTheoryMixin) method_tab.method_changed.connect(self.updateStatusBar)
[docs] def generateJaguarStatus(self): """ Create the standard Jaguar status bar message, such as "Jaguar: B3LYP / 6-31**, Single Point Energy" :return: The Jaguar status bar message :rtype: str """ molecule_tab = self.getTab(tabs.ProvidesBasisMixin) basis = molecule_tab.getBasis() method_tab = self.getTab(tabs.ProvidesTheoryMixin) method = method_tab.getMethod() jag_status = "Jaguar: %s / %s, %s" % (method, basis, self.TASK_NAME) return jag_status
[docs]class TaskStatusMixin(object): """ A mixin that will add the task name to the status bar text. No specific tabs are required. """
[docs] def generateJaguarStatus(self): """ Create a Jaguar status bar message containing the task name, similar to "Jaguar: pKa" :return: The Jaguar status bar message :rtype: str """ jag_status = "Jaguar: %s" % self.TASK_NAME return jag_status
[docs]class PropertiesMixin(object): """ A mixin that will send updates the Properties tab whenever the relevant settings in the "Theory" or "Molecule" tab change. Classes that use this mixin must have must have a "Theory" and a "Properties" tab, and must have a "Molecule" tab with a basis selector. """
[docs] def __init__(self): """ Connect a signal from the Theory tab to the Properties tab whenever the theory level is changed """ super(PropertiesMixin, self).__init__() molecule_tab = self.getTab(tabs.ProvidesBasisMixin) method_tab = self.getTab(tabs.ProvidesTheoryMixin) theory_tab = self.getTab(tabs.TheoryTab) molecule_tab.basis_changed.connect(self.updatePropertiesTab) method_tab.method_changed.connect(self.updatePropertiesTab) theory_tab.spinTreatmentChanged.connect(self.updatePropertiesTab) theory_tab.excited_state_changed.connect(self.updatePropertiesTab) theory_tab.hamiltonianChanged.connect(self.updatePropertiesTab) self.updatePropertiesTab()
[docs] def updatePropertiesTab(self): """ Notify the properties tab that the level of theory or basis set has been updated. """ molecule_tab = self.getTab(tabs.ProvidesBasisMixin) method_tab = self.getTab(tabs.ProvidesTheoryMixin) theory_tab = self.getTab(tabs.TheoryTab) properties_tab = self.getTab(tabs.PropertiesTabBase) theory_level = method_tab.getTheoryLevel() dft_functional = method_tab.getFunctional() spin_treatment = theory_tab.getSpinTreatment(theory_level) excited_state = theory_tab.getExcitedState() hamil = theory_tab.getHamiltonian() basis_full = molecule_tab.getBasis() (basis, polarization, difuse) = jag_basis.parse_basis(basis_full) properties_tab.theoryOrBasisUpdated(theory_level, dft_functional, spin_treatment, excited_state, basis, hamil)
[docs]class NoInputTabJobMixin(object): """ A mixin that will add the standard Jaguar job running and writing behavior. The write option in the gear menu will use a custom dialog box that allows the user to select between writing a .bat or an .in file. """ @af2.validator(999) # Run this validation last, since it's the function that actually does # a fair bit of the job setup @gui_utils.catch_jag_errors def updateAndCheckMmJag(self): """ Update and validate self._jag_input, the `schrodinger.application.jaguar.input.JaguarInput` object containing the settings for the input file. """ self._updateMmJag() self._runPreflight() return True
[docs] @af2.appmethods.start() def start(self): """ Launch the job. Note that `updateAndCheckMmJag` must be run before this function to update and validate self._jag_input. :return: A job object for the launched job :rtype: `schrodinger.job.jobcontrol.Job` """ return self.launchJaguarJob(self._jag_input)
[docs]class ReadEditMixin(object): """ A mixin for panels that should include Read... and Edit... in the gear menu """
[docs] def setup(self): super(ReadEditMixin, self).setup() self._previously_included_entry = None self.edit_dialog = None
[docs] @af2.appmethods.read("Read...", "Read in geometry and settings files") def read(self, allowed_options=None): """ Read in the user-specified Jaguar input file and apply it's settings to the panel. :type allowed_options: list :param allowed_options: list of allowed options for the Read dialog. Items of the list should be keys in the `schrodinger.jaguar.gui.filedialog.ReadFileDialog.OPTION_TEXT` dictionary. """ dialog = ReadFileDialog(self, allowed_options=allowed_options) if not dialog.exec(): return filename = str(dialog.selectedFiles()[0]) selected_opt = dialog.selectedOption() try: jag_input = JaguarInput(filename) except mmcheck.MmException: msg = "%s could not be processed." % filename QtWidgets.QMessageBox.critical(self, "Jaguar Error", msg) return if not gui_utils.warn_about_mmjag_unknowns(jag_input, self): return title = "Jaguar input structure" geom_options = { ReadFileDialog.GEOM_ONLY, ReadFileDialog.GEOM_AND_SETTINGS } settings_options = { ReadFileDialog.SETTINGS_ONLY, ReadFileDialog.GEOM_AND_SETTINGS } if selected_opt in geom_options: eid = self._loadStrucFromMmJag(jag_input, title) else: eid = None if selected_opt in settings_options: self._loadSettingsWithReadWarnings(jag_input, eid, title)
[docs] @af2.appmethods.custom_menu_item("Edit...", tooltip="Edit the input files") @gui_utils.catch_jag_errors def editInput(self): """ Display the edit dialog """ num_strucs = self.getNumStructures() if num_strucs > 1: err = ("The Edit dialog cannot be used when multiple structures " "are selected") raise JaguarSettingError(err) self._updateMmJag() # Don't run preflight, since the user might not be done configuring # stuff # If the input_selector is on Included Entries and the user clicks # Preview followed by Cancel, we'll need to restore the previously # included entries to the workspace, so record them now. if num_strucs == 0: self._previously_included_entry = None else: self._previously_included_entry = self._getIncludedEntryId() # Launch the edit dialog and connect the signals self.edit_dialog = self.EDIT_DLG_CLASS(self, self._jag_input) self.edit_dialog.accepted.connect(self.jagInputEdited) self.edit_dialog.run_requested.connect(self.runEditedInput) if maestro: self.edit_dialog.restore_workspace.connect( self.restorePreviouslyIncludedEntry) self.edit_dialog.show()
def _getIncludedEntryId(self): """ If the input selector is set on Included Entry, get the entry ID for the entry in the workspace (if any) so the workspace can be restored later if necessary. Otherwise, return None. Note that this function will only be called when exactly one entry is selected. :return: The entry ID for the included entry, or None. :rtype: str or NoneType """ in_sel = self.input_selector if (in_sel and in_sel.inputState() == in_sel.INCLUDED_ENTRIES): included_rows = maestro.project_table_get().included_rows eid = list(included_rows)[0].entry_id return eid else: return None
[docs] def jagInputEdited(self, jag_input, new_struc_flag): """ Process the Jaguar settings that the user entered in the Edit dialog. :param jag_input: The Jaguar settings from the Edit dialog :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` :param new_struc_flag: A flag indicating whether the structure in `jag_input` object should be loaded into the workspace. The flag must be one of: - `EditDialog.SAME_STRUCTURE`: The workspace was not changed by the Edit dialog and does not need to be changed. - `NEW_STRUCTURE`: The structure in `jag_input` is new and should be loaded into the workspace. - `RELOAD_STRUCTURE`: The structure in `jag_input` is not new, but the Edit dialog changed the workspace, so the workspace should be restored to its previous state. :type new_struc_flag: int """ # Load the new structure if there was one: if new_struc_flag == self.edit_dialog.RELOAD_STRUCTURE: eid, title = self.restorePreviouslyIncludedEntry() elif new_struc_flag == self.edit_dialog.NEW_STRUCTURE: title = "Jaguar edited structure" eid = self._loadStrucFromMmJag(jag_input, title) else: eid, title = self._getPreviousEidAndTitle() # Load the new settings if self._loadSettingsWithEditDialogWarnings(jag_input, eid, title): self.edit_dialog.close() else: self.edit_dialog.raise_()
[docs] def restorePreviouslyIncludedEntry(self): """ Restore the workspace to it's previous state if the Edit dialog didn't change the structure but did clear the workspace via a Preview. :return: A tuple of: - The entry id of the previously included entry, or None if there wasn't one - The title of the previously included entry, or None if there wasn't one :rtype: tuple """ # Clear the workspace empty_struc = structure.create_new_structure(0) maestro.workspace_set(empty_struc) if self._previously_included_entry is not None: # If the input selector is set to Included Entries, then restore the # previously included entry. We needed to set the workspace to an # empty structure so that the user doesn't get warned about erasing # the scratch workspace entry. eid = self._previously_included_entry row = maestro.project_table_get().getRow(eid) row.includeOnly() return eid, row.title else: # If the input selector is set to Selected Entries or Files and the # user ran a Preview, then just leave the workspace cleared. We # could restore the workspace to its previous state, but there's no # guarantee that would be helpful or intuitive. return None, None
def _getPreviousEidAndTitle(self): """ Get the entry ID and title of the structure that was in the workspace when the Edit dialog was opened. :return: A tuple of: - The entry id of the previously included entry, or None if there wasn't one - The title of the previously included entry, or None if there wasn't one :rtype: tuple """ eid = self._previously_included_entry if eid is not None: proj = maestro.project_table_get() title = proj[eid].title else: title = None return eid, title def _catchLoadSettingsWarnings(self, jag_input, eid, title): """ Load the specified settings and capture any warnings that are raised. See `BaseJaguarPanel._loadSettings` for argument documentation. :return: A list of warnings raised by the tabs while the specified settings are loaded. :rtype: list """ with warnings.catch_warnings(record=True) as caught_warnings: warnings.simplefilter("always", gui_utils.JaguarSettingWarning) self._loadSettings(jag_input, eid, title) # Re-raise any non-JaguarSettingWarning warnings. We primarily do this # for debugging purposes, as it makes it much clearer if one of the tabs # incorrectly raises a UserError instead of a JaguarSettingWarning. As # a nice side-effect, this also allows legitimate non-Jaguar warnings # through. jag_warnings = [] for cur_warning in caught_warnings: if isinstance(cur_warning.message, gui_utils.JaguarSettingWarning): jag_warnings.append(cur_warning) else: # The vars of a caught warning are intentionally the same as the # arguments to showwarning, but there's a private variable that # we need to strip out warning_data = { k: v for k, v in vars(cur_warning).items() if k in { "message", "category", "filename", "lineno", "file", "line" } } warnings.showwarning(**warning_data) return jag_warnings def _loadSettingsWithEditDialogWarnings(self, jag_input, eid, title): """ Load the specified settings. If any warnings are raised, they will be presented to the user in a message box. This message box will offer the option to return to the Edit dialog. See `BaseJaguarPanel._loadSettings` for argument documentation. :return: True if the Edit dialog should be closed. False if the user wishes to return to the Edit dialog. :rtype: bool """ caught_warnings = self._catchLoadSettingsWarnings(jag_input, eid, title) if caught_warnings: msg, this_value = self._mergeWarnings(caught_warnings) msg += ("\n\nTo run a job using %s, return to the Edit dialog and " u"select File \u2192 Run." % this_value) discard_values = "Discard Value" if len(caught_warnings) > 1: discard_values += "s" dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("Unrecognized values") dialog.setText(msg) dialog.addButton("Return to Edit Dialog", QtWidgets.QMessageBox.AcceptRole) dialog.addButton(discard_values, QtWidgets.QMessageBox.RejectRole) return dialog.exec() else: return True def _loadSettingsWithReadWarnings(self, jag_input, eid, title): """ Load the specified settings. If any warnings are raised, they will be presented to the user in a message box. This message box will offer the option to launch the "Jaguar - Run Input Files" panel. See `BaseJaguarPanel._loadSettings` for argument documentation. """ caught_warnings = self._catchLoadSettingsWarnings(jag_input, eid, title) if caught_warnings: msg, this_value = self._mergeWarnings(caught_warnings) msg += ("\n\n%s will be discarded. To run a job using %s, use the " "<a href='jaguarruninputfiles'>Jaguar - Run Input Files" "</a> panel" % (this_value.capitalize(), this_value)) dialog = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Warning, "Warning", msg, QtWidgets.QMessageBox.Ok) dialog.setTextFormat(Qt.RichText) dialog_lbl = dialog.findChild(QtWidgets.QLabel, "qt_msgbox_label") dialog_lbl.setOpenExternalLinks(False) dialog_lbl.linkActivated.connect(self._launchReadInputFilesPanel) dialog.exec() def _launchReadInputFilesPanel(self): """ If inside of Maestro, launch the "Jaguar - Read Input Files" panel. If outside of Maestro, inform the user that the "Jaguar - Read Input Files" is unavailable and that input files may be launched via the command line. """ try: maestro.command("pythonrunbuiltin jaguar_gui.run_input_panel") except schrodinger.MaestroNotAvailableError: print('The "Jaguar - Run Input Files" panel is not available ' 'outside of Maestro. The file may be launched via the ' 'command line using $SCHRODINGER/jaguar.') def _mergeWarnings(self, caught_warnings): """ Merge a list of warnings into a single string with a blank line between each warning. :param caught_warnings: The list of warnings to merge, as returned by `_catchLoadSettingsWarnings` :type caught_warnings: list :return: A tuple of: - A single string containing all warnings - A string of "this value" if there was one warning or "these values" if there was more than one warning """ msg_list = [str(wrn.message) for wrn in caught_warnings] msg = "\n\n".join(msg_list) this_value = ("this value" if len(caught_warnings) == 1 else "these values") return msg, this_value
[docs] def runEditedInput(self, jag_input): """ Run a job using the JaguarInput file created in the Edit dialog. :param jag_input: The JaguarInput object to run :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` """ # Note that, since we're not using the AF2 start() method, we have to do # a bit of extra work. if not self.config_dlg.validate(): # If the configuration dialog isn't in a usable state, return to the # Edit dialog. Note that config_dlg.validate() will inform the user # of the specific error. self.edit_dialog.raise_() return if self.createJobDir() is False: # Make sure that we can successfully create the job directory. # createJobDir() will inform the user of the specific error. self.edit_dialog.raise_() return self.start_mode = af2.FULL_START with fileutils.chdir(self.jobDir()): job = self.launchJaguarJob(jag_input) self.updateJobname() if maestro: maestro.job_started(job.JobID)
def _loadStrucFromMmJag(self, jag_input, name): """ Load the structure file from the specified JaguarInput object :param jag_input: The JaguarInput object to get the file from :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` :param name: The name we should give to the new Project Table entry. This name will also be used for the structure title if no structure title is set. :type name: str :return: The entry ID of the loaded structure :rtype: str """ if not jag_input.hasStructure(): return in_sel = self.input_selector if maestro: proj = maestro.project_table_get() struc = jag_input.getStructure() if not struc.title: struc.title = name row = proj.importStructure(struc, name=name, wsreplace=True) in_sel.setInputState(in_sel.INCLUDED_ENTRIES) return row.entry_id else: filename = jag_input.getMaefilename() filename = os.path.abspath(filename) in_sel.setFile(filename) return None
[docs]class MultiStructureMixin(object): """ A mixin for panels that contain a MultiStructureTab. Note that this mixin assumes that the ReadEditMixin is present, and MultiStructureMixin must be listed before ReadEditMixin in the class declaration. """
[docs] def setPanelOptions(self): """ Remove the input selector """ super(MultiStructureMixin, self).setPanelOptions() self.input_selector_options = None
[docs] def setup(self): super(MultiStructureMixin, self).setup() self.multi_struc_tab = self.getTab(tabs.MultiStructureTab)
[docs] @af2.maestro_callback.project_updated def projectUpdated(self): self.multi_struc_tab.projectUpdated() self.clearConstraintsIfMultiStrucTab()
[docs] def useParallelArg(self): return True
def _addStructureToMmJag(self): """ Override the BaseJaguarPanel function so that more than one structure can be written to the mmjag handle. """ strucs = self.multi_struc_tab.getStructures() zmats = [mm.MMJAG_ZMAT1, mm.MMJAG_ZMAT2, mm.MMJAG_ZMAT3] for (cur_struc, cur_zmat) in zip(strucs, zmats): if cur_struc is None: self._jag_input.deleteStructure(cur_zmat) else: self._cleanStructureForJagInput(cur_struc) self._jag_input.setStructure(cur_struc, cur_zmat) def _loadStrucFromMmJag(self, jag_input, name): """ Override the ReadEditMixin function so that more than one structure can be loaded. See `ReadEditMixin._loadStrucFromMmJag` for documentation on arguments and return values. """ wsreplace = True entry_ids = [] all_zmats = [mm.MMJAG_ZMAT1, mm.MMJAG_ZMAT2, mm.MMJAG_ZMAT3] eid = None proj = maestro.project_table_get() for i, zmat in enumerate(all_zmats, start=1): if not jag_input.hasStructure(zmat): entry_ids.append(None) else: struc = jag_input.getStructure(zmat) if not struc.title: struc.title = name + " (zmat %i)" % i row = proj.importStructure(struc, name=name, wsreplace=wsreplace) wsreplace = False entry_ids.append(row.entry_id) if eid is None: eid = row.entry_id self.multi_struc_tab.setStructures(entry_ids, jag_input) return eid
[docs] @af2.appmethods.custom_menu_item("Edit...", tooltip="Edit the input files") @gui_utils.catch_jag_errors def editInput(self): """ Override the ReadEditMixin function to display the edit dialog. Since we have no input selector, the checks present in ReadEditMixin.editInput() are not necessary here (and would lead to tracebacks). """ self._updateMmJag() self.edit_dialog = self.EDIT_DLG_CLASS(self, self._jag_input) self.edit_dialog.accepted.connect(self.jagInputEdited) self.edit_dialog.run_requested.connect(self.runEditedInput) self.edit_dialog.show()
[docs]class InputTabMixin(object): """ A mixin for panels that use the Input tab. This mixin removes the input selector and keeps the selected structures table up to date. Note that this mixin must appear on the inheritance list before ReadEditMixin. """
[docs] def setPanelOptions(self): """ Remove the input selector """ super(InputTabMixin, self).setPanelOptions() self.input_selector_options = None
[docs] def setup(self): """ Store a reference to the input tab and make sure that data from table delegates is commited when the gear menu is accessed """ super(InputTabMixin, self).setup() self.input_tab = self.getTab(tabs.InputTabBase) self.input_tab.projectUpdated()
[docs] @af2.maestro_callback.project_updated def projectUpdated(self): """ Update the table whenever the project updates """ self.input_tab.projectUpdated()
[docs] @af2.maestro_callback.workspace_changed 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_tab.workspaceChanged(what_changed)
[docs] def getTheoryAndSpinData(self, eid): """ Get keywords related to the theory and spin for a structure. :param eid: Entry ID to get theory and spin settings for :type eid: int :rtype: (str, dict, utils.SpinTreatment, dict) :return: method, Dictionary of theory related keywords, the spin treatment, and a dictionary of spin-related keywords """ try: method_tab = self.getTab(tabs.ProvidesTheoryMixin) theory_tab = self.getTab(tabs.TheoryTab) except ValueError: # The panel has no tabs for supplying theory level/settings. method = None theory_keywords = {} spin_treatment = None else: method = method_tab.getMethodForEid(eid) theory_level = method_tab.getTheoryLevelForEid(eid) theory_keywords = theory_tab.getMmJagKeywords(theory_level) spin_treatment = theory_tab.getSpinTreatment(theory_level) spin_keywords = self._getSettingsAffectedBySpinTreatment() return method, theory_keywords, spin_treatment, spin_keywords
def _updateMmJag(self): """ Update self._jag_input in preparation for the edit dialog. """ self._getAllJagSettings() if self.getNumStructures() > 0: # If we get here, we can assume that there's exactly one structure # loaded into the Input tab, since the check in # ReadEditMixin.editInput() ensures that there aren't multiple # structures loaded. eid, struc, keywords = self.input_tab.getStructures()[0] theory, theory_keywords, spin_treatment, spin_keywords = \ self.getTheoryAndSpinData( eid) keywords = self._applySpinKeywords(keywords, spin_treatment, *spin_keywords) keywords.update(theory_keywords) self._addMmJagStruc(eid, struc, keywords) else: self._clearPerAtomSettings() self._jag_input.deleteStructure(mm.MMJAG_ZMAT1) def _addMmJagStruc(self, eid, struc, keywords): """ Add the specified structure and keywords to self._jag_input :param eid: The entry id of the structure to add :type eid: str :param struc: The structure to add :type struc: `schrodinger.structure.Structure` :param keywords: The mmjag keywords to add :type keywords: dict """ self._clearPerAtomSettings() self._cleanStructureForJagInput(struc) self._jag_input.setStructure(struc, mm.MMJAG_ZMAT1) self._jag_input.setValues(keywords) self._getPerAtomSettings(eid) def _getIncludedEntryId(self): """ Get the entry ID for the structure in the Input table. Note that this function will only be called when exactly one entry is selected. :return: The entry ID :rtype: str """ eid, struc, keywords = self.input_tab.getStructures()[0] return eid def _getSettingsAffectedBySpinTreatment(self): """ Get all mmjag values that need to be set on a per-structure basis dependent on the spin treatment and the spin multiplicity. :return: A tuple of: - A dictionary of {keyword: value} for all settings required for an unrestricted spin treatment - A dictionary of {keyword: value} for all settings required for a restricted spin treatment :rtype: tuple """ try: properties_tab = self.getTab(tabs.PropertiesTabBase) except ValueError: return {}, {} else: return properties_tab.getSettingsAffectedBySpinTreatment() def _applySpinKeywords(self, keywords, spin_treatment, unres_keywords, res_keywords): """ Add the appropriate spin-treatment-related keywords to the per-structure keyword dictionary :param keywords: The per-structure keyword dictionary for the current structure. If this dictionary does not contain a spin multiplicity setting (mm.MMJAG_IKEY_MULTIP), then no changes will be made. :type keywords: dict :param spin_treatment: The spin treatment setting. If this setting is not `utils.SpinTreatment.Automatic`, then no changes will be made to `keywords`. :type spin_treatment: `utils.SpinTreatment` :param unres_keywords: A keyword dictionary to add to `keywords` if the structure requires an unrestricted spin treatment (if the spin multiplicity is greater than one). :type unres_keywords: dict :param res_keywords: A keyword dictionary to add to `keywords` if the structure requires an restricted spin treatment (if the spin multiplicity is one). :type res_keywords: dict :return: The updated keywords dictionary :rtype: dict :note: The input `keywords` dictionary will be modified in place as a side-effect of this function. Ensure that the contents of the original dictionary are no longer needed after this function call. """ if (spin_treatment is gui_utils.SpinTreatment.Automatic and mm.MMJAG_IKEY_MULTIP in keywords): if keywords[mm.MMJAG_IKEY_MULTIP] > 1: keywords.update(unres_keywords) else: keywords.update(res_keywords) return keywords
[docs]class InputTabJobMixin(InputTabMixin): """ A mixin for panels that use the Input tab and launch "standard" Jaguar jobs (i.e. one .in file per structure). Implies InputTabMixin. Note that this mixin must appear on the inheritance list before ReadEditMixin. """ def _saveAllInputs(self, jobname): """ Create one input file per structure specified in the input tab. :param jobname: The job name. This will be used as the filename (after adding ".in") if there is only one structure. Otherwise, it will be ignored. :type jobname: str :return: A list of saved filenames :rtype: list """ filenames = [] add_keywords = self._parseKeywordsLE() if self.getNumStructures() == 1: infile = "%s.in" % (jobname) eid, struc, keywords = self.input_tab.getStructures()[0] method, theory_keywords, spin_treatment, spin_keywords = \ self.getTheoryAndSpinData(eid) keywords = self._applySpinKeywords(keywords, spin_treatment, *spin_keywords) keywords.update(theory_keywords) self._saveInput(eid, struc, keywords, infile, add_keywords) filenames.append(infile) else: for eid, struc, keywords in self.input_tab.getStructures(): basis = self._getBasisForFilename(eid) method, theory_keywords, spin_treatment, spin_keywords = \ self.getTheoryAndSpinData(eid) infile = gui_utils.generate_job_name(struc.title, self.SHORT_NAME, method, basis) infile += ".in" keywords = self._applySpinKeywords(keywords, spin_treatment, *spin_keywords) keywords.update(theory_keywords) self._saveInput(eid, struc, keywords, infile, add_keywords) filenames.append(infile) return filenames def _getBasisForFilename(self, eid): """ Get the basis name for use in the input file filename for the specified structure. :param eid: The entry id to get the basis name for :type eid: str :return: If there are per-atom basis sets for the specified structure, returns "mixed". If there are no per-atom basis sets, returns the basis name. If there is are no basis settings in the panel, returns None. :rtype: str or NoneType """ if isinstance(self.input_tab, tabs.ProvidesBasisMixin): basis = self.input_tab.getBasisForEid(eid, "mixed") basis = str(basis) return basis else: return None def _saveInput(self, eid, struc, keywords, infile, add_keywords={}): # noqa: M511 """ Create a Jaguar .in file based on self._jag_input using the specified structure and structure. :param eid: The entry id :type eid: str :param struc: The structure :type struc: `schrodinger.structure.Structure` :param keywords: The Jaguar keywords to add to self._jag_input :type keywords: dict :param add_keywords: Additional keywords to apply to keywords last :type add_keywords: dict :param infile: The filename to save to :type infile: str """ self._addMmJagStruc(eid, struc, keywords) # do not modify the self._jag_input save_jag_input = copy.copy(self._jag_input) # copy the _structures dict in order to generate the needed .mae files save_jag_input._structures = self._jag_input._structures save_jag_input.setValues(add_keywords) save_jag_input.saveAs(infile, follow_links=mm.MMJAG_APPEND_X_OVERWRITE, validate=True)
[docs] @af2.appmethods.start() def start(self): """ Launch the job. Note that `_getGeneralSettings` must be run before this function to update and validate self._jag_input. :return: A job object for the launched job :rtype: `schrodinger.job.jobcontrol.Job` """ cmd = self.createCmd() cmd.extend(self.jobnameArg()) jobname = self.jobname() filenames = self._saveAllInputs(jobname) cmd.extend(filenames) return self.launchJobCmd(cmd)
@af2.validator(999) # Run this validation last, since it actually does a fair bit of the job # setup @gui_utils.catch_jag_errors def _getGeneralSettings(self): """ Get the settings that apply to all structures and load them into self._jag_input (i.e. everything other than the per-structure settings specified in the Input tab). :return: True if the settings were valid, False otherwise. (Note that the False value is returned in catch_jag_errors.) :rtype: bool """ self._getAllJagSettings() self._runPreflight() return True
[docs] def getNumStructures(self): return self.input_tab.getNumStructures()
def _loadStrucFromMmJag(self, jag_input, name): """ Load the structure file from the specified JaguarInput object :param jag_input: The JaguarInput object to get the file from :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` :param name: The name we should give to the new Project Table entry. This name will also be used for the structure title if no structure title is set. :type name: str :return: The entry id of the newly added project table row :rtype: str """ maestro.command("entryunselectall") if not jag_input.hasStructure(): return proj = maestro.project_table_get() struc = jag_input.getStructure() if not struc.title: struc.title = name row = proj.importStructure(struc, name=name, wsreplace=False) row.is_selected = True row.includeOnly() return row.entry_id
[docs]class InputMultiplicityMixin(object): """ Base class for any mixin that needs to get list of entries with spin multiplicities greater than one. It defines input tab and provides function that returns list of names for the entries that have spin multiplicity greater than one. """
[docs] def setup(self): """ Initialize mixin. """ super(InputMultiplicityMixin, self).setup() self.input_tab = self.getTab(tabs.InputTabBase)
def _getEntriesWithMultiplicity(self): """ Returns a list of names of the entries that have multiplicity larger than 1. :return: list of entry names :rtype: list """ names = [] for eid, struc, data in self.input_tab.getStructures(): title = struc.title if data[mm.MMJAG_IKEY_MULTIP] > 1: names.append(title) return names
[docs]class PropertiesValidatorMixin(InputMultiplicityMixin): """ A mixin that verifies that all selected properties can be calculated for the input entries. The check is done for certain properties that can not be calculated for entries with spin multiplicity > 1. Classes that use this mixin must have both "Input" and "Properties" tabs. """
[docs] def setup(self): """ Initialize Mixin. """ super(PropertiesValidatorMixin, self).setup() self.property_tab = self.getTab(tabs.PropertiesTabBase)
@af2.validator() def checkMultiplicityProperty(self): """ This function creates a list of entries for which some properties can not be calculated. A warning message is presented that shows both entry names and a list of offending properties. :return: True if all calculations can be run, otherwise returns a tuple that contains False and a string with informative message. :rtype: bool or tuple """ props = self.property_tab.getSpecialProperties() if not props: return True names = self._getEntriesWithMultiplicity() if not names: return True warning_str = self._getWarningText(names, props) return (False, warning_str) def _getWarningText(self, names, props): """ This function creates a text of warning message that will contain names of entries with multiplicity > 1 and a list of properties that can not be calculated for these entries. :param names: list of entry names :type names: list :param props: list of properties :type props: list """ names_str = "\n".join(names) props_str = "\n".join(props) s = ("Calculation can not be run for these properties:\n\n" "%s \n\nbecause the spin multiplicity for the following " "entries is greater than 1: \n\n%s " % (props_str, names_str)) return s
[docs]class ExcitedStateValidatorMixin(InputMultiplicityMixin): """ A mixin that verifies that there are no input structures with spin multiplicity greater than one when excited state toggle is set in theory tab. Classes that use this mixin must have both "Input" and "Theory" tabs. """
[docs] def setup(self): """ Initialize Mixin. """ super(ExcitedStateValidatorMixin, self).setup() self.theory_tab = self.getTab(tabs.TheoryTab)
@af2.validator() def checkMultiplicity(self): """ When excited state toggle is checked in theory tab, construct list of entries with spin multiplicity greater than one. If such entries are found a warning message is presented that shows their names. :return: True if all calculations can be run, otherwise returns a tuple that contains False and a string with informative message. :rtype: bool or tuple """ excited_state_info = self.theory_tab.getExcitedState() spin_unrestricted = True for theory_level in self.method_tab.getAllUsedTheoryLevels(): spin_treatment = self.theory_tab.getSpinTreatment(theory_level) spin_unrestricted = spin_unrestricted and spin_treatment.unrestrictedAvailable( ) if not excited_state_info.excited_state or spin_unrestricted: return True names = self._getEntriesWithMultiplicity() if not names: return True warning_str = self._getMultiplicityWarningText(names) return (False, warning_str) def _getMultiplicityWarningText(self, names): """ This function creates a text of warning message that will contain names of entries with multiplicity > 1. :param names: list of entry names :type names: list """ names_str = "\n".join(names) s = ("Calculation can not be run for the following entries with " "the spin multiplicity greater than 1 when SCF " "spin is restricted in the theory tab: \n\n%s" % names_str) return s
[docs]class UseConsistentOrbitalsMixin: """ A mixin for panels where the SCF tab contains a "Use consistent orbitals" checkbox. Note that this mixin requires InputTabJobMixin and that UseConsistentOrbitalsMixin must appear before InputTabJobMixin in the inheritance list. This mixin also requires panels to have a ProvidesTheoryMixin tab (typically the Input tab) and a Theory tab. """
[docs] def setup(self): super().setup() try: self.method_tab = self.getTab(tabs.ProvidesTheoryMixin) self.theory_tab = self.getTab(tabs.TheoryTab) except ValueError: raise RuntimeError( "UseConsistentOrbitalsMixin requires a ProvidesTheoryMixin tab " "and a Theory tab")
[docs] @af2.appmethods.start() def start(self): """ Launch the job. If the following criteria are met, canonical.py will be used to run the job: - The "Use consistent orbitals" checkbox is checked. - There are multiple structures selected - All structures have the same basis set Otherwise, the job will be run as a standard Jaguar job (i.e. jaguar run). Also note that `_getGeneralSettings` must be run before this function to update and validate self._jag_input. :return: A job object for the launched job :rtype: schrodinger.job.jobcontrol.Job """ scf_tab = self.getTab(tabs.ScfTabConsistentOrbitals) use_consistent = scf_tab.isUseConsistentOrbitalsChecked() same_basis = self.input_tab.getCommonBasis() # check to see whether the keyword text makes the basis or the method # the same keywords = self._parseKeywordsLE() same_basis = keywords.get(mm.MMJAG_SKEY_BASIS, same_basis) same_method = (mm.MMJAG_SKEY_DFTNAME in keywords or mm.MMJAG_IKEY_MP2 in keywords or self.method_tab.getCommonMethod() is not None) if (use_consistent and same_basis is not None and same_method and self.getNumStructures() > 1): return self._startCanonicalJob(same_basis) else: return super(UseConsistentOrbitalsMixin, self).start()
def _startCanonicalJob(self, basis): """ Launch the job using canonical.py :param basis: The basis set to specify as a command-line option :type basis: str :return: A job object for the launched job :rtype: schrodinger.job.jobcontrol.Job """ cmd = [JAGUAR_EXE, "run", "canonical.py"] cmd.extend(self.jobnameArg()) cmd.extend(self._buildCommandLineKeywordArgs(basis)) filename = self.jobname() + ".mae" cmd.append(filename) self._createCanonicalInputStruc(filename) return self.launchJobCmd(cmd) def _buildCommandLineKeywordArgs(self, basis): """ Build the command line arguments for the mmjag keywords in the form "-keywords=key=value". The settings in self._jag_input will be used for all settings other than basis set. :param basis: The basis set to specify :type basis: str :return: A list of command line arguments :rtype: list """ functional = self.method_tab.getCommonFunctional() keywords = { mm.MMJAG_SKEY_BASIS: basis, mm.MMJAG_SKEY_DFTNAME: functional } theory_level = self.method_tab.getTheoryLevel() theory_keywords = self.theory_tab.getMmJagKeywords(theory_level) keywords.update(theory_keywords) keywords.update(self._parseKeywordsLE()) tmp_jag_input = copy.copy(self._jag_input) tmp_jag_input.setValues(keywords) non_default_keywords = tmp_jag_input.getNonDefault() args = [] for (key, val) in non_default_keywords.items(): cur_arg = "-keyword=%s=%s" % (key, val) args.append(cur_arg) return args def _createCanonicalInputStruc(self, filename): """ Create an .mae input file for canonical.py that contains all input structures. For each structure, molecular charge and spin multiplicity will be specified as structure properties. (Note that canonical.py will *not* read molecular charge from the input file due to JAGUAR-5604. We set it here in case that bug is resolved.) :param filename: The filename to save the input file to :type filename: str """ writer = structure.StructureWriter(filename) for eid, struc, keywords in self.input_tab.getStructures(): charge = keywords[mm.MMJAG_IKEY_MOLCHG] spin_mult = keywords[mm.MMJAG_IKEY_MULTIP] struc.property[MOLECULAR_CHARGE_PROP] = charge struc.property[SPIN_MULT_PROP] = spin_mult writer.append(struc) writer.close()
[docs]class CoordinateDialog(QtWidgets.QDialog): """ This dialog allows user to select corrective action when the same coordinate is found in the lists of scans and constraints. """ DELETE_CONSTRAINT, DELETE_SCAN, DELETE_BOTH = list(range(3))
[docs] def __init__(self, parent, is_scan): """ Initialize dialog. :param parent: parent of this dialog. :type parent: QtCore.QObject :param is_scan: flag coordinate as scan (True) or constraint (False) :type is_scan: bool """ super(CoordinateDialog, self).__init__(parent, Qt.Dialog) self.setWindowModality(Qt.WindowModal) self.setWindowFlags((self.windowFlags() | Qt.CustomizeWindowHint) & ~Qt.WindowCloseButtonHint) self.ui = coordinate_dialog_ui.Ui_Dialog() self.ui.setupUi(self) if is_scan: self.ui.warning_lbl.setText( "You are trying to define a scan that conflicts " "with constraint already defined.") else: self.ui.warning_lbl.setText( "You are trying to define a constraint that conflicts " "with scan already defined.")
[docs] def display(self): """ Brings up the dialog and waits for the user to close it. Returns the selected option. Returns default option if the user cancels the dialog. :return: one of three possible choices (delete constraint, scan or both) :rtype: int """ result = self.exec() if result: # OK pressed if self.ui.constraint_rb.isChecked(): return self.DELETE_CONSTRAINT elif self.ui.scan_rb.isChecked(): return self.DELETE_SCAN elif self.ui.both_rb.isChecked(): return self.DELETE_BOTH return self.DELETE_CONSTRAINT
[docs]class MarkerMixin(object): """ A mixin for panels that want markers displayed only when certain tabs are active. Subclasses must define MARKER_TAB_CLASSES. :cvar MARKER_TAB_CLASSES: A list of tab classes. Markers will only be displayed when a tab on this list is active. :vartype MARKER_TAB_CLASSES: list :ivar MARKER_TABS: A list of tab instances. Markers will only be displayed when a tab on this list is active. Populated from MARKER_TAB_CLASSES. :vartype MARKER_TABS: list """ MARKER_TAB_CLASSES = []
[docs] def setup(self): super(MarkerMixin, self).setup() self.MARKER_TABS = [] for cur_tab_class in self.MARKER_TAB_CLASSES: cur_tab = self.getTab(cur_tab_class) self.MARKER_TABS.append(cur_tab) self.tab_widget.currentChanged.connect(self.showAllJaguarMarkers)
[docs] def showAllJaguarMarkers(self): """ Display markers if a marker tab is active. Hide all markers otherwise. :note: This function is named showAllJaguarMarkers() so it will override the AF2 showAllJaguarMarkers() function. Note that it doesn't always show all markers. """ if self.tab_widget.currentWidget() in self.MARKER_TABS: super(MarkerMixin, self).showAllJaguarMarkers() else: self.hideAllJaguarMarkers()
[docs]class CoordinateMarkerMixin(MarkerMixin): """ A mixin for marking constraints from the Scan or Optimization tabs. Note that this mixin should not be directly applied to a panel. Instead, the OptimizationTabMarkerMixin, ScanTabMarkerMixin, or OptimizationAndScanMixin should be used. Current limitations: - If the user changes the element of an constrained (or scanned) atom, the constraint (or scan) will not be updated. :ivar _selected_eids: A set of entry ids currently selected in the project table. If the selected entries changes, all constraints will be cleared. :vartype _selected_eids: set :ivar _selected_eid_atom_total: If there is only one entry id in `_selected_eids`, then `_selected_eid_atom_total` contains the number of atoms in that entry. If the number of atoms changes, all constraints will be cleared. If `_selected_eids` is empty of contains more than one entry id, then `_selected_eid_atom_total` is None. :vartype _selected_eid_atom_total: int or NoneType """
[docs] def setup(self): """ Initialize self._selected_entries """ super(CoordinateMarkerMixin, self).setup() self._selected_eids = set() self._selected_eid_atom_total = None self.tab_widget.currentChanged.connect(self.stopPicking) self._connectInputTabSignals()
def _connectInputTabSignals(self): """ Connect signals from the input tab. Note that this method is overridden in MultiStructurePanelMarkerMixin. """ self.input_tab = self.getTab(tabs.InputTabBase) self.input_tab.strucSourceChanged.connect( self._clearConstraintsStructureCheck)
[docs] def layOut(self): """ Once setup is complete, properly initialize the selected entries for the Scan and/or Optimization tabs """ super(CoordinateMarkerMixin, self).layOut() self.clearConstraintsIfProject()
def _connectMarkerSignals(self, tab): """ Connect the signals from the specified tab to the appropriate slots :param tab: The tab to connect signals for :type tab: `schrodinger.application.jaguar.gui.tabs.base_tab.BaseTab` """ tab.coordinateAdded.connect(self._addCoordMarker) tab.coordinateDeleted.connect(self._deleteCoordMarker) tab.allCoordinatesDeleted.connect(self._deleteAllCoordMarkers) tab.coordinateSelected.connect(self._addHighlighting) tab.coordinateDeselected.connect(self._removeHighlighting) tab.refreshMarkers.connect(self.showAllJaguarMarkers)
[docs] @af2.maestro_callback.workspace_changed def clearConstraintsIfWorkspace(self, what_changed): """ Check to see if coordinate picking is allowed or if constraints need to be cleared in response to a workspace change. This callback is needed when the user adds or deleted atoms. :param what_changed: A flag indicating what has changed in the workspace :type what_changed: str """ if what_changed in (maestro.WORKSPACE_CHANGED_EVERYTHING, maestro.WORKSPACE_CHANGED_CONNECTIVITY): self._clearConstraintsStructureCheck()
[docs] @af2.maestro_callback.project_updated def clearConstraintsIfProject(self): """ Check to see if coordinate picking is allowed or if constraints need to be cleared in response to a project table change. """ self._clearConstraintsStructureCheck()
def _clearConstraintsStructureCheck(self): """ Only allow coordinate picking if there is exactly one entry in the workspace and it is also selected in the project table. If the selected entry has changed or if the number of atoms in the selected entry has changed, clear all coordinates. """ (new_selected, new_atom_total) = self._getSelectedEidsAndLength() num_selected = len(new_selected) if num_selected == 0: err = ("No entries are selected in the project table") self._setAcceptableConstraintEids(set(), err) elif num_selected > 1: self._setAcceptableConstraintEids(set(), CONSTRAINTS_ERR) elif (new_selected != self._selected_eids or new_atom_total != self._selected_eid_atom_total): self._setAcceptableConstraintEids(new_selected, None) self._selected_eids = new_selected self._selected_eid_atom_total = new_atom_total def _getSelectedEidsAndLength(self): """ Retrieve the entry ids for the structures selected in the project table (or included in the workspace, depending on the current Input tab settings). If there is exactly one structure selected, also return the number of atoms in that structure. :return: A tuple of: - The entry ids for the structures selected in the project table as a set. If the project table is not available, will be an empty set. - If there is exactly one structure selected in the project table, the number of atoms in that structure. None otherwise. :rtype: tuple """ try: proj = maestro.project_table_get() except project.ProjectException: return (set(), None) if self.input_tab.usingSelected(): source = proj.selected_rows else: source = proj.included_rows entry_ids = {row.entry_id for row in source} if len(entry_ids) == 1: eid = next(iter(entry_ids)) num_atoms = proj[eid].getStructure(copy=False).atom_total return entry_ids, num_atoms else: return entry_ids, None def _setAcceptableConstraintEids(self, eids, picking_err): """ Pass the constraint or scan picking restrictions to the appropriate tab. :param eids: The entry ids for which coordinate picking is acceptable. :type eids: set :param picking_err: If picking should not be allowed at all, this is the text of the error that will displayed to the user. If picking is allowed, should be None. :type picking_err: str or NoneType """ self.constraint_tab.setAcceptableContraintEids(eids, picking_err) def _addCoordMarker(self, atom_nums, coordinate_type): """ Add a marker to the specified atoms. The settings from `_markerSettings` will be used to style the marker. :param atom_nums: A list of atom numbers :type atom_nums: list :param coordinate_type: Ignored. Present for compatability with the tab signal. """ atom_objs = self._getAtomsFromInts(atom_nums) marker_settings = self._getJaguarMarkerSettings(len(atom_objs)) self.addJaguarMarker(atom_objs, **marker_settings) def _optMarkerSettings(self): """ Get the marker settings to use for the Optimization tab :return: A tuple of: - The alt_color for the marker - A tuple of colors to use for marking (atoms, pairs, angles, torsions) - A tuple of icons to use for marking (atoms, pairs, angles, torsions) :rtype: tuple """ alt_color = "user4" #cyan colors = ("orange", (1.0, 0.0, 1.0), "green", "red") icons = (self.MARKER_ICONS.LOCK, self.MARKER_ICONS.SPRING, self.MARKER_ICONS.SPRING, self.MARKER_ICONS.SPRING) return alt_color, colors, icons def _scanMarkerSettings(self): """ Get the marker settings to use for the Scan tab :return: A tuple of: - The alt_color for the marker - A tuple of colors to use for marking (atoms, pairs, angles, torsions) - A tuple of icons to use for marking (atoms, pairs, angles, torsions) :rtype: tuple """ alt_color = "user4" #cyan colors = ("orange", (1.0, 0.0, 1.0), "green", "red") icons = (self.MARKER_ICONS.SCANATOM, self.MARKER_ICONS.SCANDIST, self.MARKER_ICONS.SCANANGLE, self.MARKER_ICONS.TORSIONROTATE) return alt_color, colors, icons def _markerSettings(self): """ Get the marker settings to use for newly added markers. This function must be defined in subclasses. :return: A tuple of: - The alt_color for the marker - A tuple of colors to use for marking (atoms, pairs, angles, torsions) - A tuple of icons to use for marking (atoms, pairs, angles, torsions) :rtype: tuple :note: Marker icon constants aren't defined until runtime because they're not importable outside of Maestro, so these values can't be directly set as class constants, hence this function. """ raise NotImplementedError def _getJaguarMarkerSettings(self, num_atoms): """ Get the marker settings to use for a newly added marker with the specified number of atoms. The marker settings from `_markerSettings` will be used. :param num_atoms: The number of atoms that will be marked :type num_atoms: int :return: A dictionary containing the appropriate addJaguarMarker() arguments for color, alt_color, and icon :rtype: dict """ alt_color, colors, icons = self._markerSettings() return self._applyMarkerSettings(num_atoms, alt_color, colors, icons) def _applyMarkerSettings(self, num_atoms, alt_color, colors, icons): """ Get the marker settings to use for a newly added marker with the specified number of atoms. :param num_atoms: The number of atoms that will be marked :type num_atoms: int :param alt_color: The alt_color for the marker :type alt_color: tuple, str, int, or `schrodinger.structutils.color` :param colors: A tuple of colors to use for marking (atoms, pairs, angles, torsions) :type colors: tuple :param icons: A tuple of icons to use for marking (atoms, pairs, angles, torsions) :type icons: tuple :return: A dictionary containing the appropriate addJaguarMarker() arguments for color, alt_color, and icon :rtype: dict """ settings = {} settings["alt_color"] = alt_color settings["color"] = colors[num_atoms - 1] settings["icon"] = icons[num_atoms - 1] return settings def _deleteCoordMarker(self, atom_nums): """ Remove the marker from the specified atoms :param atom_nums: A list of atom numbers :type atom_nums: list """ atom_objs = self._getAtomsFromInts(atom_nums) self.removeJaguarMarkerForAtoms(atom_objs) def _deleteAllCoordMarkers(self): """ Delete all markers that are not part of the Input subtab (Per-Atom Basis and Charge Constraint markers). """ for marker in list(self.getAllJaguarMarkers()): if not hasattr(marker, 'tab_name'): self.removeJaguarMarker(marker) def _getAtomsFromInts(self, atom_nums): """ Get the workspace atoms with the specified atom numbers :param atom_nums: A list of atom numbers :type atom_nums: list :return: A list of atom objects :rtype: list """ ws_struc = maestro.workspace_get() return [ws_struc.atom[i] for i in atom_nums] def _addHighlighting(self, atom_nums): """ Highlight the marker for the specified atoms :param atom_nums: A list of atom numbers :type atom_nums: list """ atom_objs = self._getAtomsFromInts(atom_nums) marker = self.getJaguarMarker(atom_objs) marker.setHighlight(True) def _removeHighlighting(self, atom_nums): """ Un-highlight the marker for the specified atoms :param atom_nums: A list of atom numbers :type atom_nums: list """ try: atom_objs = self._getAtomsFromInts(atom_nums) except IndexError: # If we're in the process of changing the workspace contents, then # we won't be able to get the appropriate atoms, but the markers # have been deleted anyway return try: marker = self.getJaguarMarker(atom_objs) marker.setHighlight(False) except ValueError: # If the highlighted row was just deleted, then the marker was # already deleted return
[docs] def stopPicking(self): """ Stop atom picking in response to changing tabs """ self.constraint_tab.stopPicking()
[docs]class OptimizationTabMarkerMixin(CoordinateMarkerMixin): """ A mixin for marking constraints from the Optimization tab """ MARKER_TAB_CLASSES = [tabs.OptimizationTab]
[docs] def setup(self): super(OptimizationTabMarkerMixin, self).setup() self.constraint_tab = self.getTab(tabs.OptimizationTab) self._connectMarkerSignals(self.constraint_tab)
def _markerSettings(self): return self._optMarkerSettings()
[docs]class ScanTabMarkerMixin(CoordinateMarkerMixin): """ A mixin for marking constraints from the Scan tab """ MARKER_TAB_CLASSES = [tabs.ScanTab]
[docs] def setup(self): super(ScanTabMarkerMixin, self).setup() self.constraint_tab = self.getTab(tabs.ScanTab) self._connectMarkerSignals(self.constraint_tab)
def _markerSettings(self): return self._scanMarkerSettings()
[docs]class MultiStructurePanelMarkerMixin(OptimizationTabMarkerMixin, MultiStructureMixin): """ A mixin for panels that contain a MultiStructureTab and an Optimization Tab. Note that this mixin includes MultiStructureMixin, so that does not need to be added separately. Also note that this mixin assumes that the ReadEditMixin is present, and MultiStructureMixin must be listed before ReadEditMixin in the class declaration. Current limitations: - Markers will only appear on the structure used to define the constraint. (i.e. If a constraint was defined by picking atoms in the transition state structure, markers will only appear on the transition state structure. No markers will appear on the reactant or product structure.) - Changing the structures in the Transition State or IRC tab will clear the constraints, even if the user is just selecting an additional structure. """
[docs] def setup(self): super(MultiStructurePanelMarkerMixin, self).setup() self._selected_eids = None self._selected_eid_atom_total = None self.multi_struc_tab.structureChanged.connect( self.clearConstraintsIfMultiStrucTab)
def _connectInputTabSignals(self): """ Panels with a MultiStructureTab do not have an input tab, so there's no signal to connect here. """ # This function intentionally left blank
[docs] def clearConstraintsIfProject(self): """ Since the MultiStructureTab input is not dependant on the project table, ignore project table changes. (Note that this function overrides the CoordinateMarkerMixin function of the same name. """
# This function intentionally left blank def _clearConstraintsStructureCheck(self): """ If the number of atoms in any of the structures listed in the MultiStructureTab change, clear all constraints. """ new_atom_totals = self._getAtomTotals(self._selected_eids) if new_atom_totals is None: self._selected_eids = None self._selected_eid_atom_total = None self._setAcceptableConstraintEids([], None) elif new_atom_totals != self._selected_eid_atom_total: self._setAcceptableConstraintEids(self._selected_eids, None) self._selected_eid_atom_total = new_atom_totals def _getAtomTotals(self, eids): """ Get the total number of atoms for the structures of the specified entry ids. :param eids: A list of entry ids or None :type eids: list or NoneType :return: A list of the total number of atoms for each entry id. If the project table is not avaialable or if `eids` is None, None will be returned. :rtype: list or NoneType """ if eids is None: return None try: proj = maestro.project_table_get() except project.ProjectException: return None atom_totals = [] for cur_eid in eids: num_atoms = proj[cur_eid].getStructure(copy=False).atom_total atom_totals.append(num_atoms) return atom_totals def _updateMmJag(self): """ Update the mmjag object that will be used to build the input file. :raise JaguarSettingError: If any settings are invalid. """ self._clearPerAtomSettings() super()._updateMmJag() self._getPerAtomSettings(None)
[docs] def clearConstraintsIfMultiStrucTab(self): """ If the user changes the structures listed on the MultiStructureTab, clear all constraints. Only allow picking if the listed structures have the same number of atoms and atom names, and only allow picking for the listed structures. """ eids, acceptable = self.multi_struc_tab.getEids() if not acceptable: tab_name = self.multi_struc_tab.NAME err = ("The structures specified in the %s tab contain different " "atoms" % tab_name) self._setAcceptableConstraintEids([], err) self._selected_eids = None else: self._setAcceptableConstraintEids(eids, None) self._selected_eids = eids atom_totals = self._getAtomTotals(eids) self._selected_eid_atom_total = atom_totals
[docs]class OptimizationAndScanMixin(CoordinateMarkerMixin): """ A mixin that receives updates from Scan and Optimization tabs when new coordinate is added. It then checks whether this coordinate is defined as both scan and constraints coordinate. If this is the case a warning is shown This mixin also ensures that constraints from both the Scan and Optimization tabs are marked. Classes that use this mixin must have both "Scan" and "Optimization" tabs. """ MARKER_TAB_CLASSES = [tabs.OptimizationTab, tabs.ScanTab] SCAN = "scan" OPTIMIZATION = "optimization"
[docs] def setup(self): """ Connect the appropriate signals from Scan and Optimization tabs """ super(OptimizationAndScanMixin, self).setup() self.scan_tab = self.getTab(tabs.ScanTab) self.optimization_tab = self.getTab(tabs.OptimizationTab) self.scan_tab.coordinateAdded.connect(self.checkConstraintCoords) self.optimization_tab.coordinateAdded.connect(self.checkScanCoords) self.scan_tab.allCoordinatesDeleted.connect( lambda: self._removeAllTabMarkers(self.SCAN)) self.optimization_tab.allCoordinatesDeleted.connect( lambda: self._removeAllTabMarkers(self.OPTIMIZATION)) self._connectMarkerSignals(self.scan_tab) self._connectMarkerSignals(self.optimization_tab)
def _connectMarkerSignals(self, tab): tab.coordinateDeleted.connect(self._deleteCoordMarker) tab.coordinateSelected.connect(self._addHighlighting) tab.coordinateDeselected.connect(self._removeHighlighting) tab.refreshMarkers.connect(self.showAllJaguarMarkers)
[docs] def checkConstraintCoords(self, atoms, coordinate_type): """ This function checks whether a given coordinate entity already exists in constraint coordinates list. If it is there already a warning message is shown. """ if not self.optimization_tab.model.checkNewCoordinate( atoms, coordinate_type): self.resolveCoordsConflict(atoms, coordinate_type, True) else: self._addScanMarker(atoms)
[docs] def checkScanCoords(self, atoms, coordinate_type): """ This function checks whether a given coordinate entity already exists in scan coordinates list. If it is there already a warning message is shown. """ if not self.scan_tab.model.checkNewCoordinate(atoms, coordinate_type): self.resolveCoordsConflict(atoms, coordinate_type, False) else: self._addOptimizationMarker(atoms)
[docs] def resolveCoordsConflict(self, atoms, coordinate_type, is_scan): """ Shows dialog that prompts user to resolve conflict between scan and constraint. :param atoms: list of atom for new coordinate :type atoms: list :param coordinate_type: coordinate type :type coordinate_type: int :param is_scan: flag coordinate as scan (True) or constraint (False) :type is_scan: bool """ user_action = CoordinateDialog(self, is_scan).display() self._deleteCoordMarker(atoms) if user_action == CoordinateDialog.DELETE_CONSTRAINT: self.optimization_tab.model.removeCoordinate(atoms, coordinate_type) self._addScanMarker(atoms, True) elif user_action == CoordinateDialog.DELETE_SCAN: self.scan_tab.model.removeCoordinate(atoms, coordinate_type) self._addOptimizationMarker(atoms, True) elif user_action == CoordinateDialog.DELETE_BOTH: self.optimization_tab.model.removeCoordinate(atoms, coordinate_type) self.scan_tab.model.removeCoordinate(atoms, coordinate_type)
def _setAcceptableConstraintEids(self, eids, picking_err): self.scan_tab.setAcceptableContraintEids(eids, picking_err) self.optimization_tab.setAcceptableContraintEids(eids, picking_err) def _addCoordMarker(self, atom_nums, alt_color, colors, icons, tab_name, check_visibility=False): """ Add a marker to the specified atoms using the specified settings :param atom_nums: A list of atom numbers :type atom_nums: list :param alt_color: The alt_color for the marker :type alt_color: tuple, str, int, or `schrodinger.structutils.color` :param colors: A tuple of colors to use for marking (atoms, pairs, angles, torsions) :type colors: tuple :param icons: A tuple of icons to use for marking (atoms, pairs, angles, torsions) :type icons: tuple :param tab_name: The name of the tab that this marker is for. Must be one of `self.SCAN` or `self.OPTIMIZATION` :type tab_name: str :param check_visibility: If True, markers that don't belong to the current tab will be hidden. Only necessary if adding a marker to one tab while on a different tab. :type check_visibility: bool """ atom_objs = self._getAtomsFromInts(atom_nums) marker_settings = self._applyMarkerSettings(len(atom_objs), alt_color, colors, icons) marker = self.addJaguarMarker(atom_objs, **marker_settings) marker.tab_name = tab_name if check_visibility: self.showAllJaguarMarkers() def _addScanMarker(self, atom_nums, check_visibility=False): """ Add a Scan tab marker for the specified atoms :param atom_nums: A list of atom numbers :type atom_nums: list """ alt_color, colors, icons = self._scanMarkerSettings() self._addCoordMarker(atom_nums, alt_color, colors, icons, self.SCAN, check_visibility) def _addOptimizationMarker(self, atom_nums, check_visibility=False): """ Add an Optimization tab marker for the specified atoms :param atom_nums: A list of atom numbers :type atom_nums: list """ alt_color, colors, icons = self._optMarkerSettings() self._addCoordMarker(atom_nums, alt_color, colors, icons, self.OPTIMIZATION, check_visibility)
[docs] def showAllJaguarMarkers(self): """ Display the appropriate markers if a marker tab is active. Hide all markers otherwise. :note: This function is named showAllJaguarMarkers() so it will override the AF2 showAllJaguarMarkers() function. Note that it never actually shows all markers. """ super(OptimizationAndScanMixin, self).showAllJaguarMarkers() cur_tab = self.tab_widget.currentWidget() if cur_tab is self.scan_tab: cur_tab_name = self.SCAN elif cur_tab is self.optimization_tab: cur_tab_name = self.OPTIMIZATION else: # markers have already been hidden in MarkerMixin.showAllJaguarMarkers return for cur_marker in self.getAllJaguarMarkers(): if cur_marker.tab_name != cur_tab_name: cur_marker.hide()
def _removeAllTabMarkers(self, tab_name): """ Remove all markers for the specified tab :param tab_name: The name of the tab to delete markers for. Must be either `self.SCAN` or `self.OPTIMIZATION`. :type tab_name: str """ for marker in list(self.getAllJaguarMarkers()): if marker.tab_name == tab_name: self.removeJaguarMarker(marker)
[docs] def stopPicking(self): self.scan_tab.stopPicking() self.optimization_tab.stopPicking()
[docs]class SubTabMixin(object): """ A mixin for panels that contain an InputTab with sub-tabs. This mixin controls markers and atom picking activation/deactivation. Requires InputTabMixin and must appear before InputTabMixin on the inheritance list. """
[docs] def setup(self): super(SubTabMixin, self).setup() self.tab_widget.currentChanged.connect(self.tabChanged) self.tab_widget.currentChanged.connect(self.showAllJaguarMarkers) self.input_tab = self.getTab(tabs.InputTabWithSubTabs) self.input_tab.subTabChanged.connect(self.showAllJaguarMarkers) self.input_tab.addJaguarMarker.connect(self.addSubTabMarker) self.input_tab.removeJaguarMarker.connect(self.removeSubTabMarker) self.input_tab.setMarkerHighlighting.connect( self.setSubTabMarkerHighlighting) self.input_tab.method_changed.connect(self.theoryToInput)
[docs] @af2.appmethods.close() def deactivateTabs(self): """ Deactivate workspace atom picking in all tabs """ for cur_tab in self.tabs: cur_tab.deactivate()
[docs] def showEvent(self, event): """ When the panel is shown, activate workspace atom picking if necessary See Qt documentation for an explanation of the argument """ super(SubTabMixin, self).showEvent(event) self.tab_widget.currentWidget().activate()
[docs] def tabChanged(self, new_index): """ When the tab is changed, activate the newly selected tab :param new_index: The index of the newly selected tab :type new_index: int """ self.deactivateTabs() self.tabs[new_index].activate()
[docs] def showAllJaguarMarkers(self): """ Display only markers for the current sub-tab and apply the sub-tab- specific settings :note: This function is named showAllJaguarMarkers() so it will override the AF2 showAllJaguarMarkers() function. Note that it doesn't always show all markers. """ # TODO: Allow markers that belong to the Input tab itself (for pKa # atoms) # Allow parent classes to show/hide markers as they think appropriate super(SubTabMixin, self).showAllJaguarMarkers() cur_tab = self.tab_widget.currentWidget() if cur_tab is not self.input_tab: sub_tab_name = None eids = [] else: sub_tab_name = self.input_tab.activeSubTabName() eids = self.input_tab.displayedEntryIds() for cur_marker in self.getAllJaguarMarkers(): if hasattr(cur_marker, 'tab_name'): # This is a input subtab marker - show if this is the input # subtab that the marker belongs to and the entry id is valid show = (sub_tab_name and sub_tab_name in cur_marker.tab_name and cur_marker.eid in eids) # Always hide input subtab markers if they shouldn't be shown hide = not show else: # This is not a marker for an input subtab - hide it if an input # subtab is active hide = sub_tab_name is not None # Don't intentionally show this marker because the parent class # would have taken care of showing if necessary show = False if hide: cur_marker.hide() elif show: settings = cur_marker.tab_name[sub_tab_name] cur_marker.applySettings(settings)
[docs] def addSubTabMarker(self, atoms, settings, sub_tab_name): """ Add a workspace marker for the specified sub-tab. If a marker already exists for the given atoms, sub-tab-specific settings will be added to the marker. :param atoms: A list of atoms to add the marker for :type atoms: list :param settings: The marker settings :type settings: dict :param sub_tab_name: The name of the sub-tab that the marker is for :type sub_tab_name: str """ check_visibility = False try: marker = self.addJaguarMarker(atoms) except ValueError: marker = self.getJaguarMarker(atoms) check_visibility = True else: marker.eid = atoms[0].entry_id marker.tab_name = {} full_settings = marker.defaultSettings() full_settings.update(settings) marker.tab_name[sub_tab_name] = full_settings marker.applySettings(full_settings) if check_visibility: self.showAllJaguarMarkers()
[docs] def removeSubTabMarker(self, atoms, sub_tab_name): """ Delete a workspace marker for the specified sub-tab. If the marker applies to other sub-tabs, then only the sub-tab-specific settings for the given sub-tab will be deleted. :param atoms: A list of atoms to remove the marker for :type atoms: list :param sub_tab_name: The name of the sub-tab that the marker is for :type sub_tab_name: str """ marker = self.getJaguarMarker(atoms) del marker.tab_name[sub_tab_name] if not marker.tab_name: self.removeJaguarMarker(marker) else: self.showAllJaguarMarkers()
[docs] def setSubTabMarkerHighlighting(self, atoms, highlight, sub_tab_name): """ Change the workspace marker highlighting for the specified marker. :param atoms: A list of atoms to change the highlighting for :type atoms: list :param highlight: Whether to highlight (True) or unhighlight (False) the specified marker :type highlight: bool :param sub_tab_name: The name of the sub-tab that the marker is for :type sub_tab_name: str :note: This function assumes that the specified sub-tab is currently active. """ marker = self.getJaguarMarker(atoms) marker.tab_name[sub_tab_name]["highlight"] = highlight marker.setHighlight(highlight)
[docs] @af2.maestro_callback.project_updated def projectUpdated(self): """ Make sure that marker visibility is updated *after* the Input table has updated the included structures so that we can remove markers for structures that have been removed from the panel. """ super(SubTabMixin, self).projectUpdated() self.showAllJaguarMarkers()
[docs] def theoryToInput(self): """ When the level of theory changes, pass the new theory level to the Input tab so it can enable or disable the Charge Constraints sub-tab as necessary. """ theory_level = self.method_tab.getTheoryLevel() self.input_tab.theoryChanged(theory_level)
[docs]class AutomaticJobnameMixin(object): """ A mixin that automatically generates the jobname using the current panel settings. See PANEL-2712 for additional information. """
[docs] def setup(self): """ Search for any tabs that contain jobname-relevant settings and connect signals so that the jobname updates when the appropriate settings change. """ super(AutomaticJobnameMixin, self).setup() try: self.struc_tab = self.getTab(tabs.ProvidesStructuresMixin) except ValueError: self.struc_tab = None else: self.struc_tab.structureChanged.connect(self.jobnameDataChanged) try: self.method_tab = self.getTab(tabs.ProvidesTheoryMixin) except ValueError: self.method_tab = None else: self.method_tab.method_changed.connect(self.jobnameDataChanged) self.method_tab.method_changed.connect(self.onMethodChanged) try: self.basis_tab = self.getTab(tabs.ProvidesBasisMixin) except ValueError: self.basis_tab = None else: self.basis_tab.basis_changed.connect(self.jobnameDataChanged)
[docs] def setPanelOptions(self): super(AutomaticJobnameMixin, self).setPanelOptions() self.default_jobname = "%s" self.omit_one_from_standard_jobname = True
[docs] def jobnameData(self): struc_name, theory, basis = None, None, None if self.struc_tab is not None: struc_name = self.struc_tab.getStructureTitleForJobname() if self.method_tab is not None: theory = self.method_tab.getMethod() if self.basis_tab is not None: basis = self.basis_tab.getBasis("mixed") jobname = gui_utils.generate_job_name(struc_name, self.SHORT_NAME, theory, basis) return jobname
[docs] def onMethodChanged(self): """ Perform any needed cross-tab updates based on the current method changing """ method_tab = self.getTab(tabs.ProvidesTheoryMixin) theory_tab = self.getTab(tabs.TheoryTab) if not method_tab or not theory_tab or method_tab is theory_tab: return common_theory_level = method_tab.getCommonTheoryLevel() if not common_theory_level: return theory_tab.setTheoryLevel(common_theory_level)
[docs]class StandardPanel(AutomaticJobnameMixin, InputTabJobMixin, StandardStatusMixin, PropertiesMixin, ReadEditMixin, PropertiesValidatorMixin, ExcitedStateValidatorMixin, BaseJaguarPanel): """ A convenience class for standard Jaguar panels that: - Use the standard status bar message - Contain a Theory tab, a Propertiy tab, and an Input tab with a basis selector - Have Read... and Edit... options in the gear menu """
# This class intentionally left blank
[docs]class StandardPanelNoInputTab(StandardStatusMixin, PropertiesMixin, ReadEditMixin, NoInputTabJobMixin, BaseJaguarPanel): """ A convenience class for standard Jaguar panels that use a "Molecule" tab (with a basis selector) in place of an "Input" tab. """
# This class intentionally left blank
[docs]class MultiStructurePanel(AutomaticJobnameMixin, MultiStructurePanelMarkerMixin, StandardPanelNoInputTab): """ A convenience class for standard Jaguar panels that use a MultiStructureTab. """
# This class intentionally left blank