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