Source code for schrodinger.ui.qt.appframework2.jobwidgets

import os
import sys

import schrodinger
from schrodinger.infra import jobhub
# Appframework2 modules
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import config_dialog
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import table_helper
from schrodinger.ui.qt.appframework2 import settings
from schrodinger.ui.qt.appframework2 import taskwidgets
from schrodinger.ui.qt.config_dialog import DISP_NAMES
from schrodinger.ui.qt.standard import constants

maestro = schrodinger.get_maestro()

#===============================================================================
# Constants
#===============================================================================

CONFIG_DIALOG_HELP = 'MM_TOPIC_JOB_START_DIALOG'

#===============================================================================
# Sub-widgets
#===============================================================================


[docs]class JobSettingsButton(taskwidgets.SettingsButton): """ The "gear"button from the job bar. This button connects with a job runner and provides access to the config dialog, as well as write, write STU, and reset functionality. """
[docs] def __init__(self, runner=None, **kwargs): super(JobSettingsButton, self).__init__(runner=runner, **kwargs)
[docs] def populateMenu(self): self.setToolTip('Show the run settings dialog') self.menu.addAction('Job Settings...', self.onClicked) if maestro: self.menu.addAction('Preferences...', self.maestroJobPreferences) self.menu.addSeparator() self.menu.addAction('Write', self.writeAction) if 'SCHRODINGER_SRC' in os.environ: self.menu.addAction('Write STU ZIP File', self.writeSTUAction) taskwidgets.SettingsButton.populateMenu(self)
[docs] def maestroJobPreferences(self): """ Open the Maestro preference panel with Jobs/Starting node selected. """ if maestro: maestro.command("showpanel prefer:jobs_starting")
[docs] def onClicked(self): if not self.runner: return cd = self.getConfigDialog() cd.run() if cd.requested_action == config_dialog.RequestedAction.Run: self.onStartPressed()
[docs] def writeAction(self): if not self.runner: return self.runner.write()
[docs] def writeSTUAction(self): if not self.runner: return self.runner.writeSTU()
[docs] def resetAction(self): if not self.runner: return self.runner.reset()
[docs] def getConfigDialog(self): runner = self.runner cd = ConfigDialog(runner) cd.getConfigFromRunner() return cd
[docs] def connectRunner(self, runner): super(JobSettingsButton, self).connectRunner(runner) if runner is None: return cd = self.getConfigDialog() cd.getConfigFromRunner() cd.loadPersistentOptions() cd.applyConfigToRunner()
[docs] def validateAndApplyConfig(self): cd = self.getConfigDialog() cd.getConfigFromRunner() if cd.runValidation(): cd.applyConfigToRunner() return True return False
#=============================================================================== # Job Bar #===============================================================================
[docs]class JobBar(taskwidgets.TaskBar): """ A re-implementation of the standard Schrodinger job bar. To use, simply instantiate and connect a job runner. :cvar SETTINGS_BUTTON_CLASS: the class used for the settings ("gear") button :vartype SETTINGS_BUTTON_CLASS: :class:JobSettingsButton """ SETTINGS_BUTTON_CLASS = JobSettingsButton
[docs] def __init__(self, runner=None, label_text='Job name:', button_text='Run', task_reset=True): taskwidgets.TaskBar.__init__(self, runner, label_text, button_text, task_reset=task_reset)
[docs] def setup(self): taskwidgets.TaskBar.setup(self) self.settings_btn = self.SETTINGS_BUTTON_CLASS( task_reset=self.task_reset) self.spinner = JobMonitorButton() self.toolbar = QtWidgets.QToolBar()
[docs] def layOut(self): taskwidgets.TaskBar.layOut(self) self.main_layout.addWidget(self.toolbar) if sys.platform == "darwin": style = self.styleSheet() self.setStyleSheet(style + "QToolBar{background: none; border: 0px;}") self.toolbar.addWidget(self.name_lbl) self.toolbar.addWidget(self.name_le) self.toolbar.addWidget(self.settings_btn) self.toolbar.addWidget(self.spinner) self.main_layout.addWidget(self.start_btn)
[docs] def onStartPressed(self): if not self.settings_btn.validateAndApplyConfig(): return False taskwidgets.TaskBar.onStartPressed(self)
[docs]class MiniJobBar(JobBar):
[docs] def setup(self): JobBar.setup(self) self.main_layout = QtWidgets.QVBoxLayout()
[docs] def layOut(self): taskwidgets.TaskUIWidget.layOut(self) self.main_layout.addWidget(self.name_lbl) self.run_layout = QtWidgets.QHBoxLayout() self.main_layout.addLayout(self.run_layout) self.run_layout.addWidget(self.name_le) self.run_layout.addWidget(self.start_btn) self.line = QtWidgets.QFrame() self.line.setFrameShape(QtWidgets.QFrame.HLine) self.line.setFrameShadow(QtWidgets.QFrame.Sunken) self.main_layout.addWidget(self.line) self.bottom_layout = QtWidgets.QHBoxLayout() self.bottom_layout.addStretch() self.bottom_layout.addWidget(self.settings_btn) self.bottom_layout.addWidget(self.spinner) self.main_layout.addLayout(self.bottom_layout) self.main_layout.setSpacing(3) self.bottom_layout.setSpacing(0) self.run_layout.setSpacing(0)
#=============================================================================== # Job Table (Mini monitor) #=============================================================================== IGNORE_STATUSES = set( ["finished", "killed", "completed", "incorporated", 'written']) FAILED_STATUSES = set(["died", "failed"])
[docs]class JobMonitorButton(taskwidgets.SpinToolButton): """ A re-implementation of the mini-monitor button from appframework. Instantiating this object also creates the mini-monitor itself. """
[docs] def __init__(self, runner=None): self.failed_tasks = [] self.table_widget = JobTableWidget() self.table_widget.setStyleSheet("border-width: 0") taskwidgets.SpinToolButton.__init__(self, runner) # Set up dropdown widget with mini-monitor self.drop_menu = QtWidgets.QMenu() action = QtWidgets.QWidgetAction(self.drop_menu) action.setDefaultWidget(self.table_widget) self.drop_menu.addAction(action) self.drop_menu.setContentsMargins(2, 2, 2, 2) self.drop_menu.setStyleSheet("border-width: 0") self.setMenu(self.drop_menu) self.setPopupMode(QtWidgets.QToolButton.InstantPopup) self.pressed.connect(self.setError) self.setStyleSheet('QToolButton::menu-indicator ' '{ width: 0px; border-style: none}') self.setFixedSize(constants.BOTTOM_TOOLBUTTON_HEIGHT, constants.BOTTOM_TOOLBUTTON_HEIGHT)
[docs] def connectRunner(self, runner): taskwidgets.SpinToolButton.connectRunner(self, runner) self.table_widget.connectRunner(runner)
[docs] def disconnectRunner(self): self.table_widget.disconnectRunner() taskwidgets.SpinToolButton.disconnectRunner(self)
[docs] def onRunnerStateChanged(self): taskwidgets.SpinToolButton.onRunnerStateChanged(self) for task in self.runner.tasks(): if task.status() in FAILED_STATUSES and (task not in self.failed_tasks): self.setError(True) self.failed_tasks.append(task)
[docs]class JobTableColumns(taskwidgets.TaskTableColumns): HEADERS = ['Job Name', 'Status']
[docs]class JobTableModel(taskwidgets.TaskTableModel): COLUMN = JobTableColumns @table_helper.data_method(QtCore.Qt.BackgroundRole) def _backgroundColor(self, col, task, role): # Status job attribute, except for completed jobs, for which # ExitStatus attribute is returned. jobstatus = task.status() status_enum = jobhub.get_status_string_to_enum(jobstatus) color = jobhub.get_status_color_as_hex(status_enum) return QtGui.QColor(color)
[docs]class FilteredJobTableModel(QtCore.QSortFilterProxyModel): """ A simple proxy model to filter out completed jobs, which should not appear in the mini monitor """
[docs] def __init__(self, parent=None): super(FilteredJobTableModel, self).__init__(parent) # maintain Qt4 dynamicSortFilter default self.setDynamicSortFilter(False)
[docs] def filterAcceptsRow(self, source_row, source_parent): index = self.sourceModel().index(source_row, JobTableColumns.STATUS, source_parent) return self.sourceModel().data(index) not in IGNORE_STATUSES
[docs]class JobTableWidget(taskwidgets.TaskTableWidget): """ A widget containing the job table with its proxy as well as the status label, and a button for accessing the main job monitor. """ MODEL_CLASS = JobTableModel
[docs] def setup(self): taskwidgets.TaskTableWidget.setup(self) self.proxy_model = FilteredJobTableModel() self.proxy_model.setSourceModel(self.table_model) self.table_view.setModel(self.proxy_model) self.table_view.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectRows) self.table_view.setSelectionMode( QtWidgets.QAbstractItemView.SingleSelection) self.main_layout = swidgets.SVBoxLayout() self.status_label = QtWidgets.QLabel('') self.main_monitor_btn = QtWidgets.QPushButton('Monitor...') self.main_monitor_btn.clicked.connect(self.openMainMonitor)
[docs] def layOut(self): taskwidgets.TaskTableWidget.layOut(self) self.bottom_layout = swidgets.SHBoxLayout() self.bottom_layout.addWidget(self.status_label) self.bottom_layout.addStretch() if maestro: self.bottom_layout.addWidget(self.main_monitor_btn) self.main_layout.addLayout(self.bottom_layout)
[docs] def updateStatusLabel(self): # TODO: add in the status text. pass
[docs] def openMainMonitor(self): maestro.command("showpanel monitor")
#========================================================================= # Basic ConfigDialog Class #========================================================================= CDSuper = settings.BaseOptionsPanel
[docs]class ConfigDialog(CDSuper): """ A re-implementation of the Schrodinger Job Settings dialog. """
[docs] def __init__(self, jobrunner): self.runner = jobrunner self.options = jobrunner.jobOptions() self.requested_action = config_dialog.RequestedAction.DoNothing CDSuper.__init__(self)
[docs] def setPanelOptions(self): CDSuper.setPanelOptions(self) self.title = 'Job Settings' self.help_topic = CONFIG_DIALOG_HELP
[docs] def setup(self): CDSuper.setup(self) self.setupOutputGroup() self.setupJobGroup()
[docs] def setupOutputGroup(self): if not self.options.incorporation: self.output_grp = None return self.output_grp = QtWidgets.QGroupBox('Output') self.output_layout = QtWidgets.QGridLayout() layout = self.output_layout self.output_grp.setLayout(layout) layout.addWidget(QtWidgets.QLabel('Incorporate:')) self.incorporation_selector = IncorporationSelector() self.setAlias('disp', self.incorporation_selector) layout.addWidget(self.incorporation_selector, 0, 1) layout.addItem( QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Expanding), 0, 2)
[docs] def setupJobGroup(self): self.job_grp = QtWidgets.QGroupBox('Job') self.job_layout = QtWidgets.QGridLayout() layout = self.job_layout self.job_grp.setLayout(self.job_layout) self.jobname_le = taskwidgets.TaskNameLineEdit(self.runner) self.jobname_le.textChanged.disconnect(self.jobname_le.onTextChanged) self.setAlias('jobname', self.jobname_le) layout.addWidget(QtWidgets.QLabel('Name:'), 0, 0) layout.addWidget(self.jobname_le, 0, 1, 1, 2) self.host_selector = HostSelector() self.host_selector.loadHosts(exclude_gpus=not self.options.gpus) cpus_layout = QtWidgets.QHBoxLayout() cpus_layout.addWidget(QtWidgets.QLabel('Total:')) self.cpus_sb = QtWidgets.QSpinBox() self.cpus_sb.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) cpus_layout.addWidget(self.cpus_sb) self.cpus_units_lbl = QtWidgets.QLabel('') cpus_layout.addWidget(self.cpus_units_lbl) self.onHostChanged() if self.options.host: self.setAlias('host', self.host_selector, True) layout.addWidget(QtWidgets.QLabel('Host:'), 1, 0) layout.addWidget(self.host_selector, 1, 1) self.host_selector.currentIndexChanged.connect(self.onHostChanged) if self.options.cpus: self.setAlias('cpus', self.cpus_sb, True) layout.addLayout(cpus_layout, 1, 2)
[docs] def layOut(self): CDSuper.layOut(self) if self.output_grp: self.main_layout.addWidget(self.output_grp) if self.job_grp: self.main_layout.addWidget(self.job_grp)
[docs] def run(self): self.getConfigFromRunner() saved_config = self.runner.getNextConfig() accept = self._run() # separate method for ease of testing if accept: self.applyConfigToRunner() else: # Revert job config if user cancels self.runner.setConfig(saved_config)
def _run(self): # This single line has its own method so it can easily be mocked out return CDSuper.run(self)
[docs] def getConfigFromRunner(self): config = self.runner.getNextConfig() settings = {} for alias in self.settings_aliases: settings[alias] = getattr(config, alias) self.applyAliasedSettings(settings)
[docs] def applyConfigToRunner(self): settings = self.getAliasedSettings() config = self.runner.getNextConfig() config.applySettings(settings) self.runner.setConfig(config)
[docs] def onHostChanged(self): host = self.host_selector.currentHost() self.cpus_units_lbl.setText(host.units()) self.cpus_sb.setRange(1, host.maxNum())
[docs] def getPersistenceKey(self, alias): """ Overrides the parent method so that settings keys are based on the job runner, not the config dialog class (which would result in multiple job runners sharing the same job settings). See parent class for more information. """ module = self.runner.__module__ classname = self.runner.__class__.__name__ key = '%s-%s-configdialog-%s' % (module, classname, alias) return key
#========================================================================= # Config Dialog Widgets #=========================================================================
[docs]class HostSelector(QtWidgets.QComboBox):
[docs] def loadHosts(self, hosts=None, exclude_gpus=True): self.clear() if hosts is None: hosts = config_dialog.get_hosts(excludeGPGPUs=exclude_gpus) for host in hosts: self.addItem(host.label()) self.hosts = hosts
[docs] def currentHost(self): host = self.hosts[self.currentIndex()] return host
[docs] def setCurrentHost(self, host): index = self.hosts.index(host) self.setCurrentIndex(index)
[docs] def af2SettingsGetValue(self): return self.currentHost().name
[docs] def af2SettingsSetValue(self, name): for host in self.hosts: if host.name == name: break else: self.setCurrentIndex(0) return self.setCurrentHost(host)
[docs]class IncorporationSelector(QtWidgets.QComboBox):
[docs] def __init__(self): QtWidgets.QComboBox.__init__(self) for key, name in DISP_NAMES.items(): self.addItem(name, key)
[docs] def af2SettingsGetValue(self): return self.itemData(self.currentIndex())
[docs] def af2SettingsSetValue(self, disp): index = self.findData(disp) if index == -1: raise ValueError('Disposition not found: %s' % disp) self.setCurrentIndex(index)
if __name__ == "__main__": cd = ConfigDialog() cd.run()