"""
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 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 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]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_)