Source code for schrodinger.ui.qt.config_dialog

"""
This module provides classes for config dialogs (for use with AppFramework).
These are dialogs that allow the user to specify parameters for launching a
job (jobname, host, #cpus, etc).
"""

import enum
import warnings
from collections import OrderedDict

import pyhelp

import schrodinger.job.jobcontrol as jobcontrol  # for get_hosts()
from schrodinger import project
# Install the appropriate exception handler
from schrodinger.infra import exception_handler
from schrodinger.infra import mm
from schrodinger.infra import mmjob
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.tasks.hosts import LOCALHOST  # noqa: F401
from schrodinger.tasks.hosts import LOCALHOST_GPU  # noqa: F401
from schrodinger.tasks.hosts import Gpu
from schrodinger.tasks.hosts import Host
from schrodinger.tasks.hosts import get_GPGPUs
from schrodinger.tasks.hosts import get_hosts as non_gui_get_hosts
from schrodinger.tasks.hosts import strip_gpu_from_localhost
from schrodinger.ui.qt import messagebox
from schrodinger.ui.qt.utils import suppress_signals
from schrodinger.utils import fileutils
from schrodinger.utils import mmutil
from schrodinger.utils import preferences

from . import config_dialog_open_mp_ui
from . import config_dialog_queue_ui

exception_handler.set_exception_handler()

RequestedAction = enum.Enum('RequestedAction', ['DoNothing', 'Run', 'Write'])

# Width of the #cpus & #jobs entry fields (used by JaguarConfigDialog also)
FIXED_SB_WIDTH = 60

DUMMY_GPU_HOSTNAME = '<dummy-gpu-host>'

DISP_APPEND = 'Append new entries as a new group'
DISP_APPENDINPLACE = 'Append new entries in place'
DISP_REPLACE = 'Replace existing entries'
DISP_IGNORE = 'Do not incorporate'

DISP_NAMES = OrderedDict([('append', DISP_APPEND),
                          ('appendinplace', DISP_APPENDINPLACE),
                          ('replace', DISP_REPLACE), ('ignore', DISP_IGNORE)])

DISP_FLAG_SEPARATOR = mmjob.MMJOB_JOB_DISPOSITION_FIELD_SEP
DISP_FLAG_FIT = 'fit'

HOST_PRODUCTS = 'host_products'
GPU_HOST_PRODUCTS = 'gpu_host_products'

# GpuHostProductMode options:
# SingleCpuMultipleGpu - allow single CPU or multiple GPUs (MATSCI-12038)
GpuHostProductMode = enum.Enum('GpuHostProductMode', [
    'NoGpus', 'Single', 'Multiple', 'SingleOnlyGpu', 'MultipleOnlyGpu',
    'SingleCpuMultipleGpu'
])

maestro = None
# Check for Maestro
try:
    from schrodinger.maestro import maestro
except:
    maestro = None


[docs]def get_num_nprocs(cd_params): """ Get the number of processes requested by the user :type cd_params: `schrodinger.ui.qt.appframework.StartDialogParams` :param cd_params: The current Config Dialog settings :rtype: int or None :return: The number of CPUs requested by the user if they are using the CPUs option, or the number of simultaneous subjobs if they are using the MP subjobs option. Or None if neither of these are specified. """ procs = None if 'cpus' in cd_params: # The user has only CPUs as an option, use that procs = cd_params['cpus'] elif 'openmpsubjobs' in cd_params: # The user has either CPUs or MP subjobs as options procs = cd_params['openmpsubjobs'] if not procs: # User has option of selecting mpi jobs but has not procs = cd_params['openmpcpus'] return procs
[docs]class HostProduct: """ A collection of widgets (e.g. host menu, processor spinbox, labels) for a host product and functionality to update the widgets as needed. """
[docs] def __init__(self, host_menu, cpus_sb, cpus_units_label, gpus_mode, num_processor_widget): """ :param host_menu: Host menu for this host product :type host_menu: QtWidgets.QComboBox :param cpus_sb: CPU entry field for this host product :type cpus_sb: NumProcsSpinBox or None :param cpus_units_label: Processors units label for this host product :type cpus_units_label: QtWidgets.QLabel or None :param gpus_mode: GPU mode for this host product. Should be one of GpuHostProductMode.NoGpus, GpuHostProductMode.Single, GpuHostProductMode.Multiple, GpuHostProductMode.SingleOnlyGpu or GpuHostProductMode.MultipleOnlyGpu :type gpus_mode: int :param num_processor_widget: Widget containing number of processor selection components :type num_processor_widget: QtWidgets.QWidget or None """ self.host_menu = host_menu self.cpus_sb = cpus_sb self.cpus_units_label = cpus_units_label self.gpus_mode = gpus_mode self.num_processor_widget = num_processor_widget self.host_menu.currentIndexChanged.connect(self._onHostMenuChanged) if self.cpus_sb: self.cpus_sb.valueChanged.connect(self._updateProcUnitsLabel) self._onHostMenuChanged()
def _onHostMenuChanged(self): """ Update the widgets based on the current host setting """ if self.cpus_sb is None: return host = self.host_menu.currentData() if host is None: # No Schrodinger hosts defined: return self.cpus_units_label.setText(host.units()) is_gpu = host.hostType() == host.GPUTYPE # Disable cpus_sb when product is single CPU or GPU disable_cpus_sb_gpu = (is_gpu and self.gpus_mode in (GpuHostProductMode.Single, GpuHostProductMode.SingleOnlyGpu)) disable_cpus_sb_cpu = (not is_gpu and self.gpus_mode == GpuHostProductMode.SingleCpuMultipleGpu) can_start = True if isinstance(host, DummyGpuHost): can_start = False elif disable_cpus_sb_gpu or disable_cpus_sb_cpu: self.cpus_sb.setValue(1) self.cpus_sb.setEnabled(False) else: self.cpus_sb.setEnabled(True) self.cpus_sb.setMaximum(host.maxNum()) self.cpus_sb.setVisible(can_start) self.host_menu.setEnabled(can_start) if can_start: self._updateProcUnitsLabel() else: self.cpus_units_label.setText("(GPU not available)") self.host_menu.setToolTip( "GPU hosts must be defined in the $SCHRODINGER/schrodinger.hosts file" ) def _updateProcUnitsLabel(self): """ Update the string in the processor units label depending on the number of procs being used. """ label_txt = self.cpus_units_label.text() nprocs = self.cpus_sb.value() if nprocs == 1 and label_txt.endswith('s'): self.cpus_units_label.setText(label_txt[:-1]) elif nprocs != 1 and not label_txt.endswith('s'): self.cpus_units_label.setText(label_txt + 's')
[docs]class NumProcsSpinBox(QtWidgets.QSpinBox): """ Spin box specifically for setting number of processors. """
[docs] def __init__(self, parent=None, min=1, max=10000, default=1): """ :param parent: Parent widget :type parent: `QtWidgets.QWidget` :param min: Min value for this spinbox :type min: int :param max: Max value for this spinbox :type max: int :param default: Default value for this spinbox :type default: int """ super().__init__(parent) self.setMinimum(min) self.setValue(default) self.setMaximum(max) self.setFixedWidth(FIXED_SB_WIDTH)
[docs]class ConfigDialog: """ Toplevel Qt widget that mimics the Maestro Start dialog. Configuration options set via constructor keywords are... title - Title for the dialog window. Default is '<parent_title> - Start'. command - Function to call (not used?). jobname - Initial string value for the job name entry field. Default is ''. incorporation - Display a disposition selector for Maestro incorporation. Maestro only. Default is True. allow_replace - Allow the 'Replace existing entries' incorporation option. (Default = False) allow_in_place - Allow the 'Append new entries in place' incorporation option. (Default = True) default_disp - The default disposition, if 'incorporation' is True. Must be DISP_APPEND, DISP_APPENDINPLACE, DISP_REPLACE, or DISP_IGNORE. Default is DISP_IGNORE. disp_flags - Additional Maestro job disposition flags. Currently, the only available flag is DISP_FLAG_FIT. The flags should be separated using DISP_FLAG_SEPARATOR. Default value is empty string (no flags). host - Display a pull-down menu (or a table) for selecting the host for the job. Default is True. host_products Products that will get their own host menu and #cpus box. Not compatible with cpus3. Takes a list of strings. Default is one host menu. gpu_host_products Optional map with keys being keys from host_products that should allow GPU hosts and values being what GpuHostProductMode should be set. jobentry - Display widgets for setting the job name. Default is True. cpus - Display additional options for distributed jobs, including the number of CPUs. Default is False. cpus3 - Display additional options for Desmond distributed jobs which includes 3 CPUS values: X, Y, and Z. Default is False. njobs - Display widgets for running the job as a specified number of subjobs. Default is False. adjust - Whether to display the "Adjust" checkbox. Default is False. Requires <njobs> to be True. tmpdir - Show the tmpdir for the currently selected host. Default is False. save_host - Used the last saved host as the default host. Save any new host chosen for the next start dialog from this panel. open_mp - True/False. Allow the user to specify either the total number of cpus or the number of Open MP threads and subjobs. Default is False. open_mp is mutually exclusive with cpus as well as cpus3. open_mp is incompatible with host_products. set_resources - True/False. Allow the user to set or select queue resources from the Python start panel Job parameters passed out in the StartDialogParams object upon the dialog deactivating with via a "Start" (not "Cancel") action... proj - The Project from which the job is launched (required for incorporation). "" if not in Maestro. disp - Maestro disposition. 'append' or 'ignore' if 'incorporation' is True. "" if not in Maestro. Undefined if in Maestro but 'incorporation' is False. jobname - Job name. Undefined if 'jobentry' is False. host - Main host. Undefined if 'host' option is False. njobs - Number of subjobs. Undefined if 'njobs' option is False. adjust - Whether the user checked the "Adjust" checkbox. cpus - Number of CPUs. Undefined if 'cpus' option is False. Set to 'njobs' if the "Distribute subjobs over maximum..." is chosen, otherwise set to the number of specified CPUs. cpus3 - Number of CPUs as 3 numbers: X, Y, & Z. Used by the Desmond panels. Undefined if 'cpus3' option is False. openmpcpus - Number of total Open MP CPUs if the open_mp option was used. If the open_mp options was used and threads is 0, then openmpcpus is just the number of CPUs. None if the open_mp option was not used. threads - Number of threads if the open_mp option was used and the user chose to specify the number of Open MP threads and subjobs. If the open_mp option was used but the user only specifies CPUS, threads is 0. None if the open_mp option was not used. openmpsubjobs - Maximum number of subjobs that may be run simultaneously, if the open_mp option was used. queue_resources - Queue resource options Please see the DialogParameters class below for usage instructions. """ START, SAVE, WRITE, CANCEL, HELP = ("Run", "OK", "Write", "Cancel", "Help") CPU_UNIT_LABEL = 'processors' GPU_UNIT_LABEL = 'GPUs' HOST_LABEL_TEXT = "Host:" PRODUCT_HOSTS_KEY = 'product_hosts'
[docs] def __init__(self, parent, title="", jobname="", checkcommand=None, help_topic='MM_TOPIC_JOB_START_DIALOG', **kw): """ See class docstring. Raises an Exception if the disposition specified as the default is not recognized. If pre_close_command is specified, it will be run when the user presses the Start button. The dialog is only closed if that function returns 0. """ # Get host list self.hosts = self.getHosts() # Reference to AppFramework instance: self.parent = parent self.jobname = jobname self.requested_action = RequestedAction.DoNothing self.kw = None self.help_topic = help_topic can_start = True # Get title from parent by default, but allow it to be overridden # with the title keyword. if not title: title = parent.windowTitle() + ' - Job Settings' self.dialog = QtWidgets.QDialog(parent) self.dialog.setWindowTitle(title) self.pre_close_command = checkcommand # Create a main Vertical layout which will manage all # the components in the dialog self.main_layout = QtWidgets.QVBoxLayout(self.dialog) self.main_layout.setContentsMargins(3, 3, 3, 3) self.cpus_units_label = QtWidgets.QLabel(self.CPU_UNIT_LABEL) self.num_cpus_sb = NumProcsSpinBox() # A feature flag controls whether we default to showing the select # queue resource options in the job preferences dialog set_queue_resources = mmutil.feature_flag_is_enabled( mmutil.SET_QUEUE_RESOURCES) self.account_codes = {} # mapping of account code descriptions to # codes, if there are any defined for the host # Set default options - whether or not to display options: self.options = { # 'command': None, # function to call 'jobname': '', # Default jobname 'incorporation': True, # Whether to ask to incorporate 'allow_replace': False, 'allow_in_place': True, 'default_disp': DISP_IGNORE, # Default inforporation 'disp_flags': '', # Additional Maestro job disposition flags 'host': True, # Whether to ask for a host # Whether to display a host table that supports multiple hosts HOST_PRODUCTS: None, GPU_HOST_PRODUCTS: {}, # Show a separate host menu for the given products (list of # strings) 'jobentry': True, # Whether to ask for job entry 'cpus': False, # Whether to ask for # of processors 'cpus3': False, # Whether to ask for X,Y,&Z # of processors # NOTE: Not available with cpus # options. 'njobs': False, # Whether to ask for # of subjobs 'adjust': False, # Whether to display the "Adjust" checkbox (VSW) # NOTE: the "Adjust" box is ON by # default (as needed for VSW) 'tmpdir': False, # Whether to ask for TMPDIR 'save_host': True, # Store last host chosen and use it 'viewname': None, # viewname used to identify the panel for maestro 'open_mp': False, # Whether to specify Open MP processes # Whether to show queue resources option 'set_resources': set_queue_resources } # Make sure every user-specified option is valid: for opt in kw: if opt not in self.options: raise ValueError("ConfigDialog: Invalid option: %s" % opt) # Update options with user preferences self.options.update(kw) # Make sure that given options are valid: if 'mpi' in self.options: raise ValueError("MPI support has been replaced by Open MP.") if self.options[HOST_PRODUCTS] and self.options['cpus3']: raise Exception( "cpus3 and host_products options are mutually exclusive") if self.options[HOST_PRODUCTS] and self.options['open_mp']: raise ValueError("open_mp and host_products options are mutually " "exclusive") if self.options['open_mp'] and self.options['cpus']: raise ValueError('open_mp and cpus options are mutally exclusive') if self.options[GPU_HOST_PRODUCTS]: ghp = self.options[GPU_HOST_PRODUCTS] hp = self.options[HOST_PRODUCTS] if not hp or not all(h in hp for h in ghp): raise ValueError('gpu_host_products must be a subset of ' 'host_products') # Python-1982: memorize the last host name for this script self._app_preference_handler = preferences.Preferences( preferences.SCRIPTS) self._app_preference_handler.beginGroup('appframework') # Get the calling filename try: classname = self.parent.__class__.__name__ except AttributeError: classname = 'none' try: parentmodule = self.parent.__module__ except AttributeError: parentmodule = "Generic" self.last_proc_units_prefkey = "%s-%s-procunits" % (parentmodule, classname) self.last_host_prefkey = "%s-%s-last-host" % (parentmodule, classname) self.last_cpu_prefkey = "%s-%s-last-cpu" % (parentmodule, classname) self.last_open_mp_total_cpus_prefkey = ("%s-%s-last-open_mp_cpus" % (parentmodule, classname)) self.last_open_mp_threads_prefkey = ("%s-%s-last-open_mp_threads" % (parentmodule, classname)) self.last_open_mp_subjobs_prefkey = ("%s-%s-last-open_mp_subjobs" % (parentmodule, classname)) if self.options['incorporation'] and maestro: # project incorporation gizmos self.incorp_group = QtWidgets.QGroupBox("Output", self.dialog) self.incorp_group_layout = QtWidgets.QVBoxLayout(self.incorp_group) self.incorp_group_layout.setContentsMargins(0, -1, 0, 0) self.incorp_layout = QtWidgets.QHBoxLayout() self.incorp_layout.setContentsMargins(3, -1, 3, 3) self.incorp_layout.addWidget(QtWidgets.QLabel("Incorporate:")) self.main_layout.addWidget(self.incorp_group) self.last_disp_prefkey = "%s-%s-last-disp" % (parentmodule, classname) self.disp_states = OrderedDict([ (value, key) for key, value in DISP_NAMES.items() ]) if not self.options['allow_replace']: del self.disp_states[DISP_REPLACE] if not self.options['allow_in_place']: del self.disp_states[DISP_APPENDINPLACE] if self.options['default_disp'] not in list(self.disp_states): raise ValueError("Unrecognized default disposition: '%s'" % self.options['default_disp']) self.incorp_menu = QtWidgets.QComboBox(self.dialog) for d in list(self.disp_states): self.incorp_menu.addItem(d) disp = self._app_preference_handler.get( self.last_disp_prefkey, self.options['default_disp']) if disp in list(self.disp_states): self.incorp_menu.setCurrentIndex( list(self.disp_states).index(disp)) self.incorp_layout.addWidget(self.incorp_menu) self.incorp_layout.addStretch() self.incorp_group_layout.insertLayout(0, self.incorp_layout) self.incorp_group.setLayout(self.incorp_group_layout) # job name, user, host, cpus, scrdir group if self.options['jobentry'] or \ self.options['host'] or \ self.options['cpus'] or self.options['cpus3'] or \ self.options['njobs'] or self.options['tmpdir'] or \ self.options['open_mp']: self.job_group = QtWidgets.QGroupBox("Job", self.dialog) self.job_layout = QtWidgets.QVBoxLayout(self.job_group) self.job_layout.setContentsMargins(3, -1, 3, 3) self.main_layout.addWidget(self.job_group) # job name user layout if self.options['jobentry']: self.names_layout = QtWidgets.QHBoxLayout() self.names_layout.setContentsMargins(0, 0, 0, 0) self.job_layout.addLayout(self.names_layout) # job name entry if self.options['jobentry']: self.job_name_ef = _EntryField(self.job_group, "Name:", self.jobname) # Make the job name entry field wider (and resizeable wider): self.job_name_ef._text.setMinimumWidth(250) self.jobnameChanged = self.job_name_ef._text.textChanged self.names_layout.addWidget(self.job_name_ef) if self.options['host']: # Setup host layout and the number of CPUs: can_start = self.setupHostLayout() self.hosts = self.getHosts() if self.options['njobs']: self.njobs_layout = QtWidgets.QHBoxLayout() self.njobs_layout.setContentsMargins(0, 0, 0, 0) # Display the widgets for #CPU entry. njobs_label = QtWidgets.QLabel("Separate into:", self.job_group) self.njobs_layout.addWidget(njobs_label) self.num_jobs_ef = QtWidgets.QLineEdit(self.job_group) self.num_jobs_ef.setText("1") self.num_jobs_ef.setValidator( QtGui.QIntValidator(0, 10000, self.job_group)) self.num_jobs_ef.setFixedWidth(FIXED_SB_WIDTH) self.njobs_layout.addWidget(self.num_jobs_ef) njobs_units_label = QtWidgets.QLabel("subjobs", self.job_group) self.njobs_layout.addWidget(njobs_units_label) if self.options['adjust']: self.adjust_njobs_box = QtWidgets.QCheckBox( "Adjust", self.job_group) self.adjust_njobs_box.setToolTip( "Whether to adjust the number of subjobs to create jobs of reasonable size" ) # ON default needed by VSW self.adjust_njobs_box.setChecked(True) self.njobs_layout.addWidget(self.adjust_njobs_box) # Stretch to the right of the job widgets: self.njobs_layout.addStretch() self.job_layout.addLayout(self.njobs_layout) if self.options['host']: self.job_layout.addLayout(self.queue_resources_layout) self.button_box = QtWidgets.QDialogButtonBox(QtCore.Qt.Horizontal, self.dialog) self.setUpButtonBox(can_start=can_start)
[docs] def setUpButtonBox(self, can_start=True): """ Set up the button box items for the dialog. :param can_start: If True, add a Start button. Otherwise add a Write button. :type cqan_start: bool """ self.start_button = QtWidgets.QPushButton(self.START) self.write_button = QtWidgets.QPushButton(self.WRITE) self.save_button = QtWidgets.QPushButton(self.SAVE) self.cancel_button = QtWidgets.QPushButton(self.CANCEL) self.help_button = QtWidgets.QPushButton(self.HELP) # Prevent the dialog from making either button the default when Enter is # pressed (python-1930) start_or_write_button = self.start_button if can_start else self.write_button start_or_write_button.setAutoDefault(True) self.save_button.setAutoDefault(False) self.cancel_button.setAutoDefault(False) self.button_box.addButton(start_or_write_button, QtWidgets.QDialogButtonBox.ActionRole) self.button_box.addButton(self.save_button, QtWidgets.QDialogButtonBox.ActionRole) self.button_box.addButton(self.cancel_button, QtWidgets.QDialogButtonBox.RejectRole) if self.help_topic: self.button_box.addButton(self.help_button, QtWidgets.QDialogButtonBox.HelpRole) self.main_layout.addWidget(self.button_box) self.main_layout.addStretch() self.main_layout.addWidget(self.button_box) self.start_button.clicked.connect(self.startPressed) self.write_button.clicked.connect(self.writePressed) self.save_button.clicked.connect(self.savePressed) self.help_button.clicked.connect(self.showHelp) self.cancel_button.clicked.connect(self.dialog.reject)
[docs] def showHelp(self): pyhelp.mmpyhelp_show_help_topic(self.help_topic)
[docs] def validateNumProcs(self, silent=False): """ Checks that the number of processors requested is reasonable. Here the validation is conditional on the 'cpus' option. In derived classes this may not be valid (i.e. the validation should be run regardless of the ncpus options. :type menu: QComboBox :param menu: The menu specifying the host selection to be validated :type numfield: QLineEdit :param numfield: The widget specifying the requested # of processors :type silent: bool :param silent: suppresses warning dialogs when set to True """ if self.options['host'] and (self.options['cpus'] or self.options['open_mp']): if self.options[HOST_PRODUCTS]: for product in self.options[HOST_PRODUCTS]: host_prod = self.host_prods[product] menu = host_prod.host_menu numfield = host_prod.cpus_sb host = self.currentHost(menu) if not self.validateNumCpus(host, numfield, silent): return False return True elif self.options['open_mp']: host = self.currentHost() return self.validateNumOpenMP(host, silent=silent) else: numfield = self.num_cpus_sb host = self.currentHost() return self.validateNumCpus(host, numfield, silent) return True
def _validateNumProcs(self, name, requested, available, units, silent=False): """ Validate that the number of resources is reasonable. :type name: str :param name: name of host :type requested: int :param requested: number of processors requested for use :type available: int :param available: number of processors available on name :type units: str :param units: unit string for requested and available :type silent: bool :param silent: suppresses warning dialogs when set to True """ if requested == 0: if not silent: self.warning('Number of %s cannot be set to 0.' % units) return False if requested > available: if not silent: self.warning('Number of %s requested is greater than number ' 'available on %s.' % (units, name)) return False return True
[docs] def validateNumCpus(self, host, editfield, silent=False): """ Validate number of CPUs :type host: Host :param host: the host on which the CPUs reside :type editfield: QWidget :param editfield: widget specifying the number of CPUs :type silent: bool :param silent: suppresses warning dialogs when set to True """ return self._validateNumProcs(host.name, editfield.value(), host.processors, self.CPU_UNIT_LABEL, silent)
[docs] def validateNumGpus(self, host, editfield, silent=False): """ Validate number of GPUs :type host: Host :param host: the host on which the GPUs reside :type editfield: QWidget :param editfield: widget specifying the number of GPUs :type silent: bool :param silent: suppresses warning dialogs when set to True """ max_gpgpu = host.num_gpus if host.queue: max_gpgpu = 1000000 return self._validateNumProcs(host.name, editfield.value(), max_gpgpu, self.GPU_UNIT_LABEL, silent)
[docs] def validateNumOpenMP(self, host, silent=False): """ Checks to make sure the number of requested processors and threads is consistent with what we know of the host capabilities. :type host: Host :param host: The host on which the CPUs reside :type silent: bool :param silent: suppresses warning dialogs when set to True :rtype: bool :return: True if number of processors & threads is allowed, False if not """ requested = self.getNumCpusToValidate(host.isQueue()) return self._validateNumProcs(host.name, requested, host.processors, self.CPU_UNIT_LABEL, silent=silent)
[docs] def validate(self): """ Checks the panel to make sure settings are valid. Return False if any validation test fails, otherwise return True. """ if not self.validateNumProcs(): return False if self.options['jobentry']: jobname = self.job_name_ef.text() # Verify that the jobname entry is valid: if not fileutils.is_valid_jobname(jobname): msg = fileutils.INVALID_JOBNAME_ERR % jobname self.warning(msg) return False if self.pre_close_command: if self.pre_close_command(jobname): # Non-zero value returned return False return True
[docs] def validateAndAccept(self): """ Validate the settings, and if no errors are found, close the dialog. """ if not self.validate(): return self.dialog.accept()
[docs] def savePressed(self): """ Slot for Save button """ self.requested_action = RequestedAction.DoNothing self.validateAndAccept()
[docs] def writePressed(self): """ Slot for Write button """ self.requested_action = RequestedAction.Write self.validateAndAccept()
[docs] def startPressed(self): """ Slot for OK and Run button """ self.requested_action = RequestedAction.Run self.validateAndAccept()
[docs] def setupHostLayout(self): """ Setup the host layout, including hostlist/table and numbers of cpus (including cpus3). :return: Whether the dialog should add a start button. :rtype: bool """ parent = self.job_group self.main_host_layout = QtWidgets.QVBoxLayout() self.main_host_layout.setContentsMargins(0, 0, 0, 0) self.job_layout.addLayout(self.main_host_layout) self.queue_resources_layout = QtWidgets.QVBoxLayout() self.queue_resources_layout.setContentsMargins(0, 0, 0, 0) self.main_host_layout.setSpacing(1) can_start = True if self.options[HOST_PRODUCTS]: can_start = self._setupHostProducts(parent) else: # host_products is not set. Display a single host pull-down menu: self.host_menu_layout = QtWidgets.QHBoxLayout() self.host_menu_layout.setContentsMargins(0, 0, 0, 0) host_label = QtWidgets.QLabel(self.HOST_LABEL_TEXT) self.host_menu_layout.addWidget(host_label) self.host_menu = QtWidgets.QComboBox(parent) use_host = self.getHostPref() self.setupHostCombo(self.host_menu, use_host=use_host) self.host_menu_layout.addWidget(self.host_menu) self.main_host_layout.addLayout(self.host_menu_layout) num_cpu_options = sum( [1 for x in ['cpus', 'cpus3', 'open_mp'] if self.options[x]]) if num_cpu_options > 1: raise ValueError("Options cpus, cpus3, and open_mp are " "mutually exclusive") if num_cpu_options: self.num_cpus_sb = NumProcsSpinBox() if self.options['cpus'] or self.options['cpus3']: # Display the widgets for #CPU entry. self.cpus_label = QtWidgets.QLabel("Total:", parent) self.host_menu_layout.addWidget(self.cpus_label) self.host_menu_layout.addWidget(self.num_cpus_sb) cpus = self._app_preference_handler.get(self.last_cpu_prefkey, None) if cpus: try: cpus = int(cpus) except ValueError: cpus = 1 self.num_cpus_sb.setValue(cpus) self.cpus_units_label = QtWidgets.QLabel( self.CPU_UNIT_LABEL, parent) self.host_menu_layout.addWidget(self.cpus_units_label) # update CPU limits self.host_menu.currentIndexChanged.connect(self.updateCPULimits) self.updateCPULimits() if self.options['cpus3']: # Display Desmond XYZ CPUs widgets: self.cpus3_layout = QtWidgets.QHBoxLayout() self.cpus3_layout.setContentsMargins(0, 0, 0, 0) #self.declabel = QtWidgets.QLabel(mainParent) #self.declabel.setText("The system will be domain-decomposed as follows:") cpusEfSizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) cpusEfSizePolicy.setHorizontalStretch(0) cpusEfSizePolicy.setVerticalStretch(0) self.xlabel = QtWidgets.QLabel(parent) self.xlabel.setText("x:") self.cpus3_layout.addWidget(self.xlabel) self.xcpus_sb = NumProcsSpinBox(parent) self.xcpus_sb.setSizePolicy(cpusEfSizePolicy) self.xcpus_sb.setMaximumSize(QtCore.QSize(50, 16777215)) self.cpus3_layout.addWidget(self.xcpus_sb) self.ylabel = QtWidgets.QLabel(parent) self.ylabel.setText("y:") self.cpus3_layout.addWidget(self.ylabel) self.ycpus_sb = NumProcsSpinBox(parent) self.ycpus_sb.setSizePolicy(cpusEfSizePolicy) self.ycpus_sb.setMaximumSize(QtCore.QSize(50, 16777215)) self.cpus3_layout.addWidget(self.ycpus_sb) self.zlabel = QtWidgets.QLabel(parent) self.zlabel.setText("z:") self.cpus3_layout.addWidget(self.zlabel) self.zcpus_sb = NumProcsSpinBox(parent) self.zcpus_sb.setSizePolicy(cpusEfSizePolicy) self.zcpus_sb.setMaximumSize(QtCore.QSize(50, 16777215)) self.cpus3_layout.addWidget(self.zcpus_sb) self.actual_cpus_label = QtWidgets.QLabel(parent) self.actual_cpus_label.setText("Actual CPUs/simulation: 1") self.cpus3_layout.addWidget(self.actual_cpus_label) # FIXME update the label # Stretch to the right of the cpus3 widgets: self.cpus3_layout.addStretch() self.main_host_layout.addLayout(self.cpus3_layout) self.xcpus_sb.valueChanged.connect(self.cpus3Edited) self.ycpus_sb.valueChanged.connect(self.cpus3Edited) self.zcpus_sb.valueChanged.connect(self.cpus3Edited) # FIXME upon edit update the actual_cpus_label elif self.options['open_mp']: self._setupOpenMPWidgets() # Stretch to the right of the host widgets: self.host_menu_layout.addStretch() if self.options['set_resources']: self._setupQueueWidgets() return can_start
[docs] def getHostPref(self): """ Get the stored host preference if available :return: Stored host preference if available or None :rtype: str or None """ if self.options['save_host']: use_host = self._app_preference_handler.get(self.last_host_prefkey, None) else: use_host = None return use_host
def _setupQueueWidgets(self): """ Set up all the Queue jobcontrol setting widgets """ self.queue_resources_widget = QtWidgets.QWidget() self.queue_resources_ui = config_dialog_queue_ui.Ui_Form() qrui = self.queue_resources_ui # Line shortener qrui.setupUi(self.queue_resources_widget) self.queue_resources_layout.addWidget(self.queue_resources_widget) self.queue_resources_layout.addStretch() self.host_menu.currentIndexChanged.connect(self.updateQueueResources) qrui.memory_le.editingFinished.connect(self._updateQArgs) qrui.walltime_le.editingFinished.connect(self._updateQArgs) qrui.memory_cb.toggled.connect(self._updateQArgs) qrui.walltime_cb.toggled.connect(self._updateQArgs) qrui.account_codes_cb.currentIndexChanged.connect(self._updateQArgs) self.updateQueueResources() def _setupHostProducts(self, parent): """ Set up host products widgets. :param parent: parent to use for the created widgets :type parent: QtWidgets.QWidget :return: Whether the config dialog should add a start button or not. :rtype: bool """ self.host_prods = {} # 1) Get saved user preferences for this panel use_hosts, cpu_nums = self._loadSavedHostProductPrefs() gpu_host_prods = self.options[GPU_HOST_PRODUCTS] # Host list, that unlike self.hosts, also includes the GPU hosts: all_hosts = get_hosts(excludeGPGPUs=False) can_start = True for product in self.options[HOST_PRODUCTS]: # 2) Create widgets to add this product to the dialog product_host_layout = QtWidgets.QHBoxLayout() product_host_layout.setContentsMargins(0, 0, 0, 0) if 'host' in product.lower(): # e.g. "CPU subhost" (ifd_plus_gui.py) or "Host" host_label_txt = product + ':' else: # e.g. simply "Glide" (vsw_gui.py) host_label_txt = product + ' host:' host_label = QtWidgets.QLabel(host_label_txt) product_host_layout.addWidget(host_label) host_menu = QtWidgets.QComboBox(parent) # 3) Check if this specific product supports # GPUs and populate hosts menu accordingly if product in gpu_host_prods: # For GPU host, use extended host list: gpus_mode = gpu_host_prods[product] if gpus_mode in [ GpuHostProductMode.SingleOnlyGpu, GpuHostProductMode.MultipleOnlyGpu ]: hosts = [h for h in all_hosts if h.gpu_list] if not hosts: dummy_gpu_host = DummyGpuHost() dummy_gpu_host.processors = 1 hosts = [dummy_gpu_host] can_start = False else: hosts = all_hosts else: # For CPU host, use self.hosts list: gpus_mode = GpuHostProductMode.NoGpus hosts = self.hosts self.setupHostCombo(host_menu, use_host=use_hosts.get(product, None), hosts=hosts) product_host_layout.addWidget(host_menu) # 4) Set up processor selection widgets if self.options['cpus']: num_processor_widget = QtWidgets.QWidget() num_processor_layout = QtWidgets.QHBoxLayout() num_processor_layout.setContentsMargins(0, 0, 0, 0) num_processor_widget.setLayout(num_processor_layout) product_host_layout.addWidget(num_processor_widget) cpus_label = QtWidgets.QLabel("Total:", parent) num_processor_layout.addWidget(cpus_label) cpus_sb = NumProcsSpinBox(parent) if product in cpu_nums: cpus_sb.setValue(int(cpu_nums[product])) num_processor_layout.addWidget(cpus_sb) # NOTE: This label will be dynamically changed to show the # the units of the currently selected host (GPUs/processors): cpus_units_label = QtWidgets.QLabel(self.CPU_UNIT_LABEL, parent) num_processor_layout.addWidget(cpus_units_label) else: cpus_sb = None cpus_units_label = None num_processor_widget = None # 5) Create a HostProduct object to store the product's # widgets and hook up their interactions. host_prod = HostProduct(host_menu, cpus_sb, cpus_units_label, gpus_mode, num_processor_widget) self.host_prods[product] = host_prod # Stretch to the right of the host widgets: product_host_layout.addStretch() # Add the layout for this product host to the dialog: self.main_host_layout.addLayout(product_host_layout) return can_start def _loadSavedHostProductPrefs(self): """ Populate a dictionary of previously-used hosts for host_products :return: 2-tuple of dicts, first mapping products to host pref and second mapping products to cpu pref. :rtype: tuple(dict(str:, str), dict(str: int)) """ if not self.options['save_host']: return {}, {} use_hosts = {} cpu_nums = {} key = self.last_host_prefkey + '-host_products' hstring = self._app_preference_handler.get(key, "") for item in hstring.split('@'): try: product, host = item.split('|') use_hosts[product] = host except ValueError: # Caused by empty string - that's OK pass key = self.last_cpu_prefkey + '-host_products' hstring = self._app_preference_handler.get(key, "") for item in hstring.split('@'): try: product, cpu = item.split('|') cpu_nums[product] = cpu except ValueError: # Caused by empty string - that's OK pass return use_hosts, cpu_nums def _setupOpenMPWidgets(self): """ Add all the widgets to the dialog to allow the user the option of specifying the number of Open MP threads and subjobs. """ self.open_mp_widget = QtWidgets.QWidget() self.open_mp_ui = config_dialog_open_mp_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.job_layout.addWidget(self.open_mp_widget) # Connect signals self.open_mp_ui.mp_open_mp_rb.clicked.connect(self.updateOpenMPInfo) self.open_mp_ui.mp_cpus_rb.clicked.connect(self.updateOpenMPInfo) self.open_mp_ui.mp_threads_sb.valueChanged.connect( self.updateOpenMPInfo) self.open_mp_ui.mp_max_subjobs_sb.valueChanged.connect( self.updateOpenMPInfo) # Preferences for storing values cpu_key = self.last_open_mp_total_cpus_prefkey pref_processes = self._app_preference_handler.get(cpu_key, 1) threads_key = self.last_open_mp_threads_prefkey pref_threads = self._app_preference_handler.get(threads_key, 0) subjobs_key = self.last_open_mp_subjobs_prefkey pref_subjobs = self._app_preference_handler.get(subjobs_key, 0) if not pref_subjobs: self.open_mp_ui.mp_cpus_rb.setChecked(True) self.num_cpus_sb.setValue(pref_processes) else: self.open_mp_ui.mp_open_mp_rb.setChecked(True) self.open_mp_ui.mp_threads_sb.setValue(pref_threads) self.open_mp_ui.mp_max_subjobs_sb.setValue(pref_subjobs) self.updateOpenMPInfo()
[docs] def updateCPULimits(self): """ This method is called whenever host selection is changed. It updates maximum number of allowed CPUs. """ if not hasattr(self, 'host_menu') or not hasattr(self, 'num_cpus_sb'): return if self.num_cpus_sb and self.host_menu.count() > 0: host = self.currentHost() # Do nothing if host is not defined (for example, when running # unit tests). if host is None: return max_cpus = host.processors if self.isGPUHost(): max_cpus = host.num_gpus self.num_cpus_sb.setMaximum(max_cpus)
[docs] def updateOpenMPInfo(self): """ Show/Hide the proper frames and update the processors label """ if self.open_mp_ui.mp_cpus_rb.isChecked(): self.open_mp_ui.mp_open_mp_grouping.hide() self.open_mp_ui.mp_cpus_grouping.show() else: self.open_mp_ui.mp_open_mp_grouping.show() self.open_mp_ui.mp_cpus_grouping.hide() self.updateOpenMPLabel()
[docs] def getTotalOpenMPCPUs(self): """ Compute the total number of Open MP CPUs to use based on the number of threads and subjobs the user entered :rtype: int :return: total number of CPUs """ threads = self.open_mp_ui.mp_threads_sb.value() subjobs = self.open_mp_ui.mp_max_subjobs_sb.value() return threads * subjobs
[docs] def getNumCpusToValidate(self, is_queue): """ Return the maximum number of processors that the job could potentially use, for validation. :param bool is_queue: If True, return number of threads per subjob requested, if False return number of threads * number of subjobs. """ if is_queue: # For queued hosts, check only maximum threads, since queuing system # will manage subjobs. return self.open_mp_ui.mp_threads_sb.value() else: # For non-queued host, do not allow jobs where #threads * #cpus # is greater than number of processors on the host. return self.getTotalOpenMPCPUs()
def _queueMemoryFixup(self, val): """ This makes sure memory is not over the maximum allowed, and that an empty space was not entered. """ qrui = self.queue_resources_ui # Line shortener memory_text = str(qrui.memory_le.text()).strip() if memory_text == "": qrui.memory_le.setText("0") qrui.memory_cb.setChecked(False) self._updateQArgs() return if float(memory_text) > self.max_memory: qrui.memory_le.setText(str(self.max_memory)) self._updateQArgs() def _queueWalltimeFixup(self, val): """ This verifies that an empty space wasn't entered """ qrui = self.queue_resources_ui # Line shortener walltime_text = str(qrui.walltime_le.text()).strip() if walltime_text == "": qrui.walltime_le.setText("0") qrui.walltime_cb.setChecked(False) self._updateQArgs()
[docs] def updateQueueResources(self): """ This updates the queue resources display when the host has changed. """ qrui = self.queue_resources_ui # Line shortener curr_host = self.currentHost() if not curr_host: return host = curr_host.name qrui.memory_frame.setVisible(True) try: memory_info = mmjob.mmjob_host_get_memory(host) default_mem = memory_info[0] / 1000 max_mem = memory_info[1] / 1000 qrui.memory_le.setText("%.4f" % default_mem) qrui.memory_label.setText("GB (maximum %.4f GB)" % max_mem) validator = QtGui.QDoubleValidator(0, max_mem, 3) validator.setNotation(QtGui.QDoubleValidator.StandardNotation) validator.fixup = self._queueMemoryFixup qrui.memory_le.setValidator(validator) self.max_memory = max_mem except mm.MmException: memory_info = None qrui.memory_cb.setChecked(False) qrui.memory_frame.setVisible(False) qrui.walltime_frame.setVisible(True) try: walltime_info = mmjob.mmjob_host_get_walltime(host) qrui.walltime_le.setText("%s" % walltime_info[0]) qrui.walltime_label.setText("minutes (maximum %s minutes)" % walltime_info[1]) validator = QtGui.QIntValidator(0, walltime_info[1]) validator.fixup = self._queueWalltimeFixup qrui.walltime_le.setValidator(validator) except mm.MmException: walltime_info = None qrui.walltime_cb.setChecked(False) qrui.walltime_frame.setVisible(False) try: (acct_names, acct_text) = mmjob.mmjob_host_get_accountcodes(host) self.account_codes = dict(list(zip(acct_text, acct_names))) qrui.account_codes_cb.addItems(["<none>"] + acct_text) qrui.account_codes_frame.setVisible(True) except mm.MmException: self.account_codes = {} qrui.account_codes_frame.setVisible(False) self.queue_resources_widget.setVisible(True) if ((memory_info is None and walltime_info is None) or not mmjob.mmjob_host_maestrocontrols_swig(host)): self.queue_resources_widget.setVisible(False) self._updateQArgs()
def _updateQArgs(self): """ This updates the QArgs line when any relevant option has changed. """ qrui = self.queue_resources_ui # Line shortener curr_host = self.currentHost() if not curr_host: return host = curr_host.name qargs = [] if qrui.memory_cb.isChecked(): memory = int(float(qrui.memory_le.text()) * 1000) qargs.append(mmjob.mmjob_host_get_qargs_memory(host, memory)) if qrui.walltime_cb.isChecked(): walltime = int(qrui.walltime_le.text()) qargs.append(mmjob.mmjob_host_get_qargs_walltime(host, walltime)) if self.account_codes: code_text = str(qrui.account_codes_cb.currentText()) code = self.account_codes.get(code_text) if code: qargs.append(mmjob.mmjob_host_get_qargs_accountcode(host, code)) qargs_text = " ".join(qargs) qrui.qargs_le.setText(qargs_text)
[docs] def updateOpenMPLabel(self): """ Update the Open MP label with the current number of processors requested """ total = self.getTotalOpenMPCPUs() self.open_mp_ui.mp_total_cpus_lbl.setText('(total = %d CPUs)' % total)
[docs] def setupHostCombo(self, combo, use_host=None, hosts=None): combo.clear() if hosts is None: hosts = self.hosts with suppress_signals(combo): for host in hosts: combo.addItem(host.label(), host) if host.name == use_host: self._selectComboText(combo, host.label()) self.updateCPULimits()
[docs] def cpus3Edited(self, ignored=None): cpus = self.xcpus_sb.value() * \ self.ycpus_sb.value() * self.zcpus_sb.value() self.actual_cpus_label.setText("Actual CPUs/simulation: %i" % cpus) self.num_cpus_sb.setValue(cpus)
[docs] def activate(self): """ Display the dialog and return the dialog parameters as as StartDialogParam object. If the dialog was cancelled then return None and restore the prior state. """ oldsettings = self.getSettings() result = self.dialog.exec() # Cancelled : return None if result == QtWidgets.QDialog.Rejected: self.applySettings(oldsettings) return None # Otherwise we are procesing the dialog: bundle up the settings # and return: return self.getSettings()
[docs] def getSettings(self, extra_kws=None): if not extra_kws: kw = {} else: kw = extra_kws # Add -PROJ and -DISP flags to invocation if we are in a project if maestro: try: pt = maestro.project_table_get() except project.ProjectException: # Use a blank project name if we were called during a project # close kw['proj'] = "" else: kw['proj'] = pt.project_name if self.options['incorporation']: value = str(self.incorp_menu.currentText()) kw['disp'] = self.disp_states.get(value) if self.options['disp_flags'] and kw['disp'] != 'ignore': kw['disp'] = DISP_FLAG_SEPARATOR.join( [kw['disp'], self.options['disp_flags']]) self._app_preference_handler.set(self.last_disp_prefkey, value) else: # Incorporation option not requested: # Override the default value of 'append' kw['disp'] = 'ignore' # Ev:59317 else: kw['proj'] = "" kw['disp'] = "" if self.options['jobentry']: # JOBNAME requestd jobname = self.job_name_ef.text() kw['jobname'] = jobname if self.options['host']: # HOST requested if self.options[HOST_PRODUCTS]: product_hosts = {} product_hosts_text = {} host_preflist = [] cpu_preflist = [] for product in self.options[HOST_PRODUCTS]: menu = self.host_prods[product].host_menu menutext = menu.currentText() host = menu.currentData() if host is None: # No schrodinger.hosts file present continue product_hosts_text[product] = menutext host_name = strip_gpu_from_localhost(host.name) if self.options['cpus']: cpus = self.host_prods[product].cpus_sb.value() product_hosts[product] = "%s:%i" % (host_name, cpus) cpu_preflist.append("%s|%s" % (product, cpus)) else: product_hosts[product] = host_name host_preflist.append("%s|%s" % (product, host_name)) kw['product_hosts'] = product_hosts kw['product_hosts_text'] = product_hosts_text host_key = self.last_host_prefkey + '-host_products' host_pref = '@'.join(host_preflist) cpu_key = self.last_cpu_prefkey + '-host_products' cpu_pref = '@'.join(cpu_preflist) else: menutext = self.host_menu.currentText() host = self.host_menu.currentData().name kw['host_text'] = str(menutext) host = strip_gpu_from_localhost(host) kw['host'] = host host_key = self.last_host_prefkey host_pref = host # Whether the select host has GPUs: host_obj = self.currentHost() # host_obj will be None if no schrodinger.hosts is installed. if host_obj and host_obj.num_gpus > 0: kw['gpus'] = [gpu.index for gpu in host_obj.gpu_list] if self.options['save_host']: self._app_preference_handler.set(host_key, host_pref) if self.options['njobs']: # NJOBS requested kw['njobs'] = int(self.num_jobs_ef.text()) if self.options['adjust']: kw['adjust'] = bool(self.adjust_njobs_box.isChecked()) if self.options['cpus']: # CPUS requested if not self.options[HOST_PRODUCTS]: kw['cpus'] = self.num_cpus_sb.value() cpu_pref = str(kw['cpus']) cpu_key = self.last_cpu_prefkey if self.options['save_host']: self._app_preference_handler.set(cpu_key, cpu_pref) if self.options['cpus3']: # CPUS XYZ requested kw['cpus3'] = ( self.xcpus_sb.value(), self.ycpus_sb.value(), self.zcpus_sb.value(), ) if self.options['open_mp']: openmpcpus, threads, openmpsubjobs = self.getOpenMPSettings() kw['openmpcpus'] = openmpcpus kw['threads'] = threads kw['openmpsubjobs'] = openmpsubjobs cpus_key = self.last_open_mp_total_cpus_prefkey self._app_preference_handler.set(cpus_key, kw['openmpcpus']) threads_key = self.last_open_mp_threads_prefkey self._app_preference_handler.set(threads_key, kw['threads']) subjobs_key = self.last_open_mp_subjobs_prefkey self._app_preference_handler.set(subjobs_key, kw['openmpsubjobs']) if self.options['viewname']: kw['viewname'] = self.options['viewname'] if self.options.get('set_resources') and self.options['host'] and not \ self.options[HOST_PRODUCTS]: qrui = self.queue_resources_ui queue_resources_args = str(qrui.qargs_le.text()) kw['queue_resources'] = queue_resources_args qsettings = {} qsettings['memory_cb'] = qrui.memory_cb.isChecked() qsettings['memory_le'] = qrui.memory_le.text() qsettings['walltime_cb'] = qrui.walltime_cb.isChecked() qsettings['walltime_le'] = qrui.walltime_le.text() qsettings['account_code'] = qrui.account_codes_cb.currentText() kw['queue_settings'] = qsettings start_params = StartDialogParams() start_params.update(kw) self.kw = kw return start_params
[docs] def getOpenMPSettings(self): """ Based on Open MP settings, return a tuple of: * Maximum number of CPUs to use * Number of threads to use. * Maximum number of subjobs to create. :return: (#cpus, #threads, #subjobs) :rtype: (int, int, int) """ if self.open_mp_ui.mp_cpus_rb.isChecked(): # User did not break down the number of threads/subjobs openmpcpus = self.num_cpus_sb.value() threads = 0 openmpsubjobs = 0 else: openmpcpus = self.getTotalOpenMPCPUs() threads = self.open_mp_ui.mp_threads_sb.value() openmpsubjobs = self.open_mp_ui.mp_max_subjobs_sb.value() return openmpcpus, threads, openmpsubjobs
def _applySetting(self, setter, settings, prop): """ Applies a specific setting via a call to the setter. :type setter: callable :param setter: a method taking a single argument to set the field :type settings: StartDialogParams :param settings: saved dialog settings :type prop: str :param prop: a dictionary key to access the desired item in settings """ if hasattr(settings, prop): data = getattr(settings, prop) if data is None: return try: setter(data) except TypeError: setter(str(data)) def _selectComboText(self, combo, text): """ Select the item in a combobox matching the specified text. If text is not found, the selection is left unchanged. :type combo: QComboBox :param combo: a combo box to search and set :type text: str :param text: text to search for within the items of combo """ index = combo.findText(text) if index != -1: combo.setCurrentIndex(index)
[docs] def applySettings(self, settings): """ Set dialog state using previously-saved parameters :type settings: StartDialogParams :param settings: saved dialog settings """ if self.options['jobentry']: # JOBNAME requestd self._applySetting(self.job_name_ef.setText, settings, 'jobname') if self.options['host']: # HOST requested if self.options[HOST_PRODUCTS]: product_hosts_text = {} self._applySetting(product_hosts_text.update, settings, 'product_hosts_text') for product in self.options[HOST_PRODUCTS]: menu = self.host_prods[product].host_menu menutext = product_hosts_text.get(product) if menutext is not None: self._selectComboText(menu, menutext) else: if hasattr(settings, 'host_text'): self._selectComboText(self.host_menu, settings.host_text) if self.options['njobs']: # NJOBS requested self._applySetting(self.num_jobs_ef.setText, settings, 'njobs') if self.options['adjust']: self._applySetting(self.adjust_njobs_box.setChecked, settings, 'adjust') if self.options['cpus']: # CPUS requested if not self.options[HOST_PRODUCTS]: self._applySetting(self.num_cpus_sb.setValue, settings, 'cpus') if self.options['cpus3']: # CPUS XYZ requested if hasattr(settings, 'cpus3') and settings.cpus3 is not None: self.xcpus_sb.setValue(settings.cpus3[0]) self.ycpus_sb.setValue(settings.cpus3[1]) self.zcpus_sb.setValue(settings.cpus3[2]) if self.options.get('set_resources') and self.options['host'] and not \ self.options[HOST_PRODUCTS]: qrui = self.queue_resources_ui qsettings = settings.queue_settings if qsettings: qrui.memory_cb.setChecked(qsettings['memory_cb']) if qsettings['memory_cb']: qrui.memory_le.setText(qsettings['memory_le']) qrui.walltime_cb.setChecked(qsettings['walltime_cb']) if qsettings['walltime_cb']: qrui.walltime_le.setText(qsettings['walltime_le']) account_code = qsettings.get('account_code') if account_code: self._selectComboText(qrui.account_codes_cb, account_code)
[docs] def warning(self, text): """ Display a warning window with the specified text. """ QtWidgets.QMessageBox.warning(self.dialog, "Warning", text)
[docs] def error(self, text): """ Show an error message with the specified text. :param str msg: Error to show. """ messagebox.show_error(parent=self.dialog, text=text)
[docs] def getHosts(self, ncpus=True, excludeGPGPUs=True): """ Returns list of host entries from appropriate schrodinger.hosts file, with parenthetical entry of the number of available processors (if 'ncpus' is True). If excludeGPGPUs is True, hosts with GPGPUs will be excluded from the list """ return get_hosts(ncpus, excludeGPGPUs)
[docs] def currentHost(self, menu=None): """ Returns the host currently selected in the menu parameter. If none is given, use self.host_menu. currentHost() can be overridden to use a different menu by default. :param menu: Menu to check for current host :type menu: `QtWidgets.QComboBox` """ if menu is None: menu = self.host_menu current_host = menu.currentData() if current_host is None: self.setupHostCombo(menu) current_host = menu.currentData() return current_host
[docs] def getHostType(self): host = self.currentHost() if host: return host.hostType()
[docs] def isGPUHost(self): return self.getHostType() == Host.GPUTYPE
[docs] def isCPUHost(self): return self.getHostType() == Host.CPUTYPE
def _formJaguarCPUFlags(self, use_parallel_flag=True): """ Determine the command line flags for an Open MP job. :type use_parallel_flag: bool :param use_parallel_flag: Whether requesting CPUs > 1 without specifying threads > 1 should be represented by the use of the -PARALLEL X flag (True, default) or -HOST host:X (False). -PARALLEL is a Jaguar flag and may not be appropriate for other programs. :return: The appropriate command line flags. :rtype: list """ cd_params = self.getSettings() return form_jaguar_cpu_flags(cd_params.host, cd_params.openmpcpus, cd_params.openmpsubjobs, cd_params.threads, use_parallel_flag=use_parallel_flag)
[docs]class GPUConfigDialog(ConfigDialog): """ Subclass of the ConfigDialog that shows only GPU hosts. """ HOST_LABEL_TEXT = "GPU host:"
[docs] def getHosts(self): """ Return a list of GPU hosts :return: List of GPU hosts :rtype: list """ hosts = super().getHosts(excludeGPGPUs=False) return [h for h in hosts if h.hostType() == Host.GPUTYPE]
# # SCHRODINGER ENTRY FIELD ########## # class _EntryField(QtWidgets.QWidget): """ A special composite widget which contains a labeled line edit field. """ def __init__(self, parent, label_text, initial_text=""): """ Create a labeled text entry area with text 'label_text', set the initial text value to 'initial_text' and if 'units_text' is defined then add a label after the editable text to display the lable """ QtWidgets.QWidget.__init__(self, parent) self._layout = QtWidgets.QHBoxLayout(self) self._layout.setContentsMargins(0, 0, 0, 0) self._label = QtWidgets.QLabel(label_text, self) self._layout.addWidget(self._label) self._text = QtWidgets.QLineEdit(self) self._text.setText(initial_text) self._layout.addWidget(self._text, 10) # Make entry field stretchable def setText(self, text): """ Set the text for the QLineEdit part of the entry field """ self._text.setText(text) def text(self): """ Returns the text for the QLineEdit part of the entry field """ return str(self._text.text())
[docs]class DummyGpuHost(Host): """ A dummy host to allow users to write job files to launch elsewhere when a GPU host is not available in their hosts file. """
[docs] def __init__(self): gpu_list = [Gpu(0, 'DummyGpu')] super().__init__(name=DUMMY_GPU_HOSTNAME, num_gpus=1, gpulist=gpu_list)
[docs]def get_hosts(ncpus=True, excludeGPGPUs=True): """ Return a list of Host objects for use in config dialogs. Note these are a subclass of jobcontrol.Host which has additional features for text labels and accounting for GPUs. If schrodinger.hosts file is missing, only localhost will be returned. If it is unreadable, then an error message will be shown in a dialog box, and an empty list will be returned. :param ncpus: whether host text labels should include number of processors :type ncpus: bool :param excludeGPGPUs: whether to exclude GPU hosts from the list :type excludeGPGPUs: bool :return: a list of Host objects :rtype: list """ try: return non_gui_get_hosts(ncpus=ncpus, excludeGPGPUs=excludeGPGPUs) except jobcontrol.UnreadableHostsFileException: # If hosts file is not readable, show an error message PANEL-6101 # and return an empty list, because job launching won't work. current_window = QtWidgets.QApplication.activeWindow() QtWidgets.QMessageBox.warning( current_window, "Cannot Read Host File", "The Schrodinger host file is invalid, jobs will not be able " "to launch. Please fix the error and restart Maestro.\n" "See the terminal for the specific error message.") return []
[docs]def gpu_hosts_available(): """ Determines whether any GPU host is available. :return: returns True if any GPU host is available and False otherwise. :rtype: bool """ hosts = get_hosts(excludeGPGPUs=False) for host in hosts: if get_GPGPUs(host.name): return True return False
[docs]def get_host_from_hostname(hostname): """ :param hostname: The name of the desired host object. :type hostname: str :return: The host object associated with a host name. :rtype: Host """ if hostname == DUMMY_GPU_HOSTNAME: return DummyGpuHost() hosts = get_hosts(excludeGPGPUs=False) return next((host for host in hosts if host.name == hostname), None)
[docs]class StartDialogParams(object): """ A collection of parameter values from the StartDialog class. """
[docs] def __init__(self): """ Initialize. The defaults are used for options that were not requested njobs is not currently used as there is no uniform way to set it """ self.jobname = None self.proj = None self.disp = None self.proc_units = None self.host = None # Host name self.product_hosts = None self.driverhost = None self.cpus = None self.cpus3 = None self.product_cpus = None self.openmpcpus = None self.threads = None self.openmpsubjobs = None self.njobs = None self.adjust = None self.viewname = None self.queue_resources = None self.queue_settings = None
[docs] def update(self, params): """ Update the param's attributes based on the given dictionary. """ self.__dict__.update(params)
[docs] def commandLineArgs(self, include_njobs=True, add_cpus=True): """ Convert this set of start dialog parameters into the canonical jobcontrol command line argument list. Generally used by AF1 panels. :rtype: list :return: list of job control command line flags """ opts = [] if self.proj: # if in Maestro opts += ['-PROJ', self.proj] if self.disp: # if in Maestro opts += ['-DISP', self.disp] if self.viewname and maestro: opts += ['-VIEWNAME', self.viewname] if self.openmpcpus: opts += self.formJaguarCPUFlags() elif self.host: # FIXME what to do if cpus3 is set? if self.cpus is not None and self.cpus > 1 and add_cpus: opts += ['-HOST', '%s:%s' % (self.host, self.cpus)] else: opts += ['-HOST', self.host] if self.njobs and include_njobs: opts += ['-NJOBS', str(self.njobs)] if self.queue_resources: opts.extend(['-QARGS', self.queue_resources]) return opts
[docs] def formJaguarCPUFlags(self): """ Determine the command line flags as requested by the user if openmp=True was used in creating the dialog. Used by AF1 panels. :rtype: list :return: The requested command line flags """ return form_jaguar_cpu_flags(self.host, self.openmpcpus, self.openmpsubjobs, self.threads)
[docs] def commandLineOptions(self): """ Convert this set of start dialog parameters into the canonical jobcontrol command line options. Generally used by AF1 panels. NOTE: Does NOT export NJOBS for backward compatibility. """ opts = self.commandLineArgs(include_njobs=False) cmd = '' for arg in opts: # Quotes around arguments with spaces are required: # (came up with Windows having usernames with spaces) cmd += ' "%s"' % arg return cmd
[docs]def form_jaguar_cpu_flags(host, cpus, subjobs, threads, use_parallel_flag=True): """ Determine the command line flags for an Open MP job. NOTE: This function is also used in af2.py. :param host: The host name :type host: str :param cpus: The number of CPUs requested. If `subjobs` and `threads` are non-zero, this value will be equal to `subjobs * threads` and can be ignored. :type cpus: int :param subjobs: The number of subjobs requested. Will be 0 if the user only specified the total number of CPUs. :type subjobs: int :param threads: The number of threads requested. Will be 0 if the user only specified the total number of CPUs. :type threads: int :type use_parallel_flag: bool :param use_parallel_flag: Whether requesting CPUs > 1 without specifying threads > 1 should be represented by the use of the -PARALLEL X flag (True) or -HOST host:X (False). -PARALLEL is a Jaguar flag and may not be appropriate for other programs. :return: The appropriate command line flags. :rtype: list """ if threads: return ['-HOST', '%s:%s' % (host, subjobs), '-TPP', str(threads)] elif cpus > 1: if use_parallel_flag: return ['-HOST', host, '-PARALLEL', str(cpus)] else: return ['-HOST', '%s:%s' % (host, cpus)] else: return ['-HOST', '%s:1' % host]
[docs]class StartDialog(ConfigDialog): START = "Start"
[docs] def __init__(self, *args, **kwargs): if 'jobentry' not in kwargs: kwargs['jobentry'] = True ConfigDialog.__init__(self, *args, **kwargs) self.button_box.removeButton(self.save_button) warnings.warn( "StartDialog is deprecated for ConfigDialog. You are " "seeing this message because you use AppFramework in a non default way. " "Strongly consider adopting the method of joblaunching in PYTHON-1795. ", DeprecationWarning)
[docs]class JobParameters: """ Class for holding job parameters. Required by AppFrameworkFrame. """
[docs] def __init__(self): """ All attributes are set directly after the instance is created. """
[docs] def printout(self): """ Print out the job parameters. """ for k in list(self.__dict__): print("%s: %s" % (k, self.__dict__[k]))
# # SCHRODINGER DIALOG PARAMETER CLASS ### #
[docs]class DialogParameters: """ Class for holding dialog parameters. Required by AppFramework Frame Dialogs. When creating an AppFramework instance, keyword 'dialogs' can be sent with dictionary. This dictionary should hold another dictionary of options for each dialog the user wants to set options for, and the key for that dictionary should be the name of the dialog. Example:: dialogs = { 'start': { 'jobname': 'my_job', 'cpus': 0, }, 'read': { 'filetypes': [('Input Files', '*.in'),], }, 'write': {}, } Options need not be set upon creation of the AppFramework instance, however. You can set options at any point, causing the next call for that dialog to generate itself with the new options. The DialogParameters instance can be found as:: <AppFramework instance>.dialog_param Thus if I wanted to turn off the number of cpus option in the start dialog, I would have:: <AppFramework instance>.dialog_param.start['cpus'] = 0 or to change the file type for the read dialog:: <AppFramework instance>.dialog_param.read['filetypes'] = [('<Type>', '<.ext>')] See the individual Dialog classes for the supported configuration options. """
[docs] def __init__(self): """ See class docstring. Read dialogs parameters (askopenfilename options) are set to:: 'initialdir': '.' 'filetypes': [('Input Files', '*.in')] by default. """ self.start = {} self.write = {} self.read = { 'initialdir': '.', # initial directory 'filetypes': [('Input Files', '*.in')] # permitted file types }
[docs] def update(self, dict): """ Built in function for updating the DialogParameters class. Passing a dictionary of the values that need to be changed or added will change current values if he key already exists, or add a new key/value pair if it doesn't. Thus, if I wanted to change the start dialog behavior with regard to jobname and tmpdir, I would probably do something like:: dict = { "start": { 'jobname': '<my_new_jobname>', 'tmpdir': 1, } } <DialogParameters object>.update(dict) The next time I brought up the dialog, the changes will have been made. """ self.__dict__.update(dict)
[docs] def set(self, dialog, **kw): """ As an alternative to the update() method, I could change the same start dialog options with the command: <DialogParameters object>.set('start', jobname = '<my_new_jobname>', tmpdir = 1) The next time I brought up the dialog, the changes will have been made. """ dict_ = {} dict_[dialog] = kw self.update(dict_)