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
from past.utils import old_div

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.Qt.QtCore import Qt
from schrodinger.tasks.hosts import get_hosts as non_gui_get_hosts
from schrodinger.tasks.hosts import LOCALHOST
from schrodinger.tasks.hosts import LOCALHOST_GPU
from schrodinger.tasks.hosts import Gpu
from schrodinger.tasks.hosts import Host
from schrodinger.tasks.hosts import get_GPGPUs
from schrodinger.tasks.hosts import strip_gpu_from_localhost
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'])

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'
# (MATSCI-9114): a dummy APPEND method which replicates DISP_APPEND
DISP_APPENDNEW = 'Append new entries'

DISP_NAMES = OrderedDict(
    [('append', DISP_APPEND), ('appendinplace', DISP_APPENDINPLACE),
     ('appendnew', DISP_APPENDNEW), ('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 = enum.Enum(
    'GpuHostProductMode',
    ['NoGpus', 'Single', 'Multiple', 'SingleOnlyGpu', 'MultipleOnlyGpu'])

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


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


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.
    """

    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)
        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
        can_start = True
        if isinstance(host, DummyGpuHost):
            can_start = False
        elif is_gpu and self.gpus_mode in [
                GpuHostProductMode.Single, GpuHostProductMode.SingleOnlyGpu
        ]:
            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')


class NumProcsSpinBox(QtWidgets.QSpinBox):
    """
    Spin box specifically for setting number of processors.
    """

    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(60)


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:"

    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,
            'allow_append': 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 not self.options['allow_append']:
                del self.disp_states[DISP_APPEND]
            # Added dummy DISP_APPENDNEW; This should not be shown
            # if this is not specifically used
            if self.options['default_disp'] != DISP_APPENDNEW:
                del self.disp_states[DISP_APPENDNEW]
            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(40)
            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)

    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)

    def showHelp(self):
        pyhelp.mmpyhelp_show_help_topic(self.help_topic)

    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 abailable
        :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

    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)

    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)

    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.getTotalOpenMPCPUs()
        return self._validateNumProcs(
            host.name,
            requested,
            host.processors,
            self.CPU_UNIT_LABEL,
            silent=silent)

    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

    def validateAndAccept(self):
        """
        Validate the settings, and if no errors are found, close the dialog.
        """
        if not self.validate():
            return
        self.dialog.accept()

    def savePressed(self):
        """
        Slot for Save button
        """
        self.requested_action = RequestedAction.DoNothing
        self.validateAndAccept()

    def writePressed(self):
        """
        Slot for Write button
        """
        self.requested_action = RequestedAction.Write
        self.validateAndAccept()

    def startPressed(self):
        """
        Slot for OK and Run button
        """
        self.requested_action = RequestedAction.Run
        self.validateAndAccept()

    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

    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()

    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)

    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()

    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

    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()

    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 = old_div(memory_info[0], 1000)
            max_mem = old_div(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)

    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)

    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()

    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)

    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()

    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)
                # MATSCI-9114: a dummy APPEND method which replicate DISP_APPEND
                if kw['disp'] == DISP_APPENDNEW:
                    kw['disp'] = DISP_APPEND
                # END of Change MATSCI-9114
                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']:
            if self.open_mp_ui.mp_cpus_rb.isChecked():
                # User did not break down the number of threads/subjobs
                kw['openmpcpus'] = self.num_cpus_sb.value()
                kw['threads'] = 0
                kw['openmpsubjobs'] = 0
            else:
                kw['openmpcpus'] = self.getTotalOpenMPCPUs()
                kw['threads'] = self.open_mp_ui.mp_threads_sb.value()
                kw['openmpsubjobs'] = self.open_mp_ui.mp_max_subjobs_sb.value()
            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

    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)

    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)

    def warning(self, text):
        """ Display a warning window with the specified text. """

        QtWidgets.QMessageBox.warning(self.dialog, "Warning", text)

    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)

    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

    def getHostType(self):
        host = self.currentHost()
        if host:
            return host.hostType()

    def isGPUHost(self):
        return self.getHostType() == Host.GPUTYPE

    def isCPUHost(self):
        return self.getHostType() == Host.CPUTYPE


class GPUConfigDialog(ConfigDialog):
    """
    Subclass of the ConfigDialog that shows only GPU hosts.
    """
    HOST_LABEL_TEXT = "GPU host:"

    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())


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.
    """

    def __init__(self):
        gpu_list = [Gpu(0, 'DummyGpu')]
        super().__init__(name=DUMMY_GPU_HOSTNAME, num_gpus=1, gpulist=gpu_list)


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 []


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


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)


class StartDialogParams(object):
    """
    A collection of parameter values from the StartDialog class.
    """

    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

    def update(self, params):
        """
        Update the param's attributes based on the given dictionary.
        """

        self.__dict__.update(params)

    def commandLineArgs(self, include_njobs=True, add_cpus=True):
        """
        Convert this set of start dialog parameters into the canonical
        jobcontrol command line argument list.

        :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

    def formJaguarCPUFlags(self):
        """
        Determine the command line flags as requested by the user if openmp=True
        was used in creating the dialog

        :rtype: list
        :return: The requested command line flags
        """

        return form_jaguar_cpu_flags(self.host, self.openmpcpus,
                                     self.openmpsubjobs, self.threads)

    def commandLineOptions(self):
        """
        Convert this set of start dialog parameters into the canonical
        jobcontrol command line options.
        NOTE: Does NOT export NJOBS for backward compatability.
        """

        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


def form_jaguar_cpu_flags(host, cpus, subjobs, threads, use_parallel_flag=True):
    """
    Determine the command line flags for an Open MP job.

    :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]


class StartDialog(ConfigDialog):
    START = "Start"

    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)


class JobParameters:
    """
    Class for holding job parameters.  Required by AppFrameworkFrame.
    """

    def __init__(self):
        """
        All attributes are set directly after the instance is created.
        """

    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 ###
#
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.


    """

    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
        }

    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)

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