from schrodinger import get_maestro
from schrodinger.models import advanced_mappers
from schrodinger.models import mappers
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.tasks import jobtasks
from schrodinger.tasks.jobtasks import AllowedHostTypes
from schrodinger.ui.qt import basewidgets
from schrodinger.ui.qt import config_dialog
from schrodinger.ui.qt import mapperwidgets
from schrodinger.ui.qt import utils
from schrodinger.ui.qt.tasks import config_dialog_ui
from schrodinger.utils.qt_utils import suppress_signals
maestro = get_maestro()
[docs]class HostSelector(mappers.MapperMixin, basewidgets.BaseWidget):
"""
:cvar PLACEHOLDER_TEXTS: Placeholders to put in the host combo when no hosts
are available based on the currently allowed host types.
"""
model_class = jobtasks.HostSettings
VALID_STATE_PROP = 'valid_state'
VALIDATION_STYLE_SHEET = f"""
QComboBox[{VALID_STATE_PROP}="true"] {{font-style: normal}}
QComboBox[{VALID_STATE_PROP}="false"] {{font-style: italic}}
"""
PLACEHOLDER_TEXTS = {
AllowedHostTypes.CPU_ONLY: '(no CPUs found)',
AllowedHostTypes.GPU_ONLY: '(no GPUs found)',
AllowedHostTypes.CPU_AND_GPU: '(no hosts found)',
}
# The following attributes used to be accessible but are now accessed
# with methods. We raise errors if you try to access them to catch any
# cases where we might still rely on them.
@property
def host_combo(self):
raise AttributeError(
"host_combo is not accessible as an attribute. "
"To get the host combobox use getHostCombo() instead.")
@host_combo.setter
def host_combo(self, _):
raise AttributeError(
"host_combo is not accessible as an attribute. "
"To get the host combobox use getHostCombo() instead.")
@property
def units_lbl(self):
raise AttributeError(
"units_lbl is not accessible as an attribute. "
"To get the host combobox use getSubjobsUnitLabel() instead.")
@units_lbl.setter
def units_lbl(self, _):
raise AttributeError(
"units_lbl is not accessible as an attribute. "
"To get the host combobox use getSubjobsUnitLabel() instead.")
@property
def num_subjobs_sb(self):
raise AttributeError(
"num_subjobs_sb is not accessible as an attribute. "
"To get the host combobox use getNumSubjobsSpinbox() instead.")
@num_subjobs_sb.setter
def num_subjobs_sb(self, _):
raise AttributeError(
"num_subjobs_sb is not accessible as an attribute. "
"To get the host combobox use getNumSubjobsSpinbox() instead.")
[docs] def initSetUp(self):
super().initSetUp()
self._label = QtWidgets.QLabel('Host: ')
self._host_combo = mapperwidgets.MappableComboBox()
self._host_combo.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.Fixed)
self._host_combo.setStyleSheet(self._host_combo.styleSheet() +
self.VALIDATION_STYLE_SHEET)
self._total_lbl = QtWidgets.QLabel('Total:')
self._num_subjobs_sb = QtWidgets.QSpinBox()
self._units_lbl = QtWidgets.QLabel()
self._subjob_widgets = (self._units_lbl, self._num_subjobs_sb,
self._total_lbl)
[docs] def initLayOut(self):
super().initLayOut()
layout = QtWidgets.QHBoxLayout()
layout.addWidget(self.getLabel())
layout.addWidget(self.getHostCombo())
layout.addWidget(self.getSubjobsLabel())
layout.addWidget(self.getNumSubjobsSpinbox())
layout.addWidget(self.getSubjobsUnitLabel())
layout.setContentsMargins(0, 0, 0, 0)
self.host_layout = layout
self.main_layout.addLayout(self.host_layout)
[docs] def initSetDefaults(self):
super().initSetDefaults()
self._setValidState(True)
[docs] def getLabelText(self):
return self._label.text()
[docs] def setLabelText(self, new_text: str):
return self._label.setText(new_text)
[docs] def getLabel(self) -> QtWidgets.QLabel:
"""
:return: Leading host label.
"""
return self._label
[docs] def getHostCombo(self) -> mapperwidgets.MappableComboBox:
"""
:return: Host combo box.
"""
return self._host_combo
[docs] def getSubjobsLabel(self) -> QtWidgets.QLabel:
"""
:return: Sub jobs label.
"""
return self._total_lbl
[docs] def getNumSubjobsSpinbox(self) -> QtWidgets.QSpinBox:
"""
:return: Sub jobs spinbox.
"""
return self._num_subjobs_sb
[docs] def getSubjobsUnitLabel(self) -> QtWidgets.QLabel:
"""
:return: Sub jobs unit label.
"""
return self._units_lbl
def _updateUnitsLabel(self):
host = self.model.host
if host is None:
return
if not host.num_gpus:
self._units_lbl.setText('processors')
else:
self._units_lbl.setText('GPUs')
def _updateNumLimits(self):
host = self.model.host
if host is None:
return
self._num_subjobs_sb.setMinimum(1)
if not host.num_gpus:
self._num_subjobs_sb.setMaximum(host.processors)
else:
self._num_subjobs_sb.setMaximum(host.num_gpus)
def _updateHostOptions(self):
self._host_combo.clear()
if self.model.allowed_host_types is None:
self.setVisible(False)
return
self.setVisible(True)
hosts = jobtasks.get_hosts()
allowed_types = self.model.allowed_host_types
any_host_added = False
for host in hosts:
if allowed_types is AllowedHostTypes.CPU_ONLY and host.num_gpus:
continue
if allowed_types is AllowedHostTypes.GPU_ONLY and not host.num_gpus:
continue
text = self._makeHostText(host)
self._host_combo.addItem(text, host)
any_host_added = True
if not any_host_added:
self._host_combo.addItem(self.getPlaceholderText(), None)
def _makeHostText(self, host):
text = host.name
if self.model.num_subjobs is not None:
if self.model.allowed_host_types is AllowedHostTypes.CPU_ONLY:
text += f' ({host.processors})'
else:
text += f' ({host.processors}, {host.num_gpus})'
return text
[docs] def defineMappings(self):
M = self.model_class
subjobs_target = mappers.TargetSpec(self._num_subjobs_sb,
setter=self._numSubjobSetter,
datatype=None)
return [
(self._updateHostOptions, M.allowed_host_types),
(self._host_combo, M.host),
(self._onHostChanged, M.host),
(subjobs_target, M.num_subjobs)] # yapf:disable
def _numSubjobSetter(self, new_val):
if new_val is None:
return
else:
self._num_subjobs_sb.setValue(new_val)
def _setNumSubjobVisible(self, visibility):
for wdg in self._subjob_widgets:
wdg.setVisible(visibility)
def _onHostChanged(self):
self._updateSubjobSpinbox()
valid = True
if self._host_combo.count() == 1 and self._host_combo.currentText(
) == self.getPlaceholderText():
valid = False
self._setValidState(valid)
def _updateSubjobSpinbox(self):
if self.model.num_subjobs is None:
self._setNumSubjobVisible(False)
return
self._setNumSubjobVisible(True)
self._updateNumLimits()
self._updateUnitsLabel()
def _setValidState(self, valid: bool):
"""
Set whether this widget is in the valid or invalid state. The invalid
state is simply when no GPU subhosts are available and thus the combo
box would otherwise be empty. Special styling is applied. The widget is
in the valid state at all other times.
:param valid: Whether or not the widget state is valid.
"""
self._host_combo.setProperty(self.VALID_STATE_PROP, valid)
utils.update_widget_style(self._host_combo)
self._host_combo.setEnabled(valid)
[docs] def getPlaceholderText(self) -> str:
"""
Return the placeholder text for the currently allowed host types.
"""
return self.PLACEHOLDER_TEXTS.get(self.model.allowed_host_types, '')
[docs]class IncorporationSelector(ConfigWidgetMixin, basewidgets.BaseWidget):
Mode = jobtasks.IncorporationMode
MODE_TEXT = {
Mode.APPEND: config_dialog.DISP_APPEND,
Mode.APPENDINPLACE: config_dialog.DISP_APPENDINPLACE,
Mode.REPLACE: config_dialog.DISP_REPLACE,
Mode.IGNORE: config_dialog.DISP_IGNORE,
}
[docs] def initSetUp(self):
super().initSetUp()
self.incorp_combo = QtWidgets.QComboBox()
[docs] def initLayOut(self):
super().initLayOut()
self.main_layout.addWidget(self.incorp_combo)
def _onConfigClassSet(self):
super()._onConfigClassSet()
with suppress_signals(self.incorp_combo):
self._setUpIncorpCombo()
def _setUpIncorpCombo(self):
Config = self.model_class
self.incorp_combo.clear()
if Config.incorporation is None:
return
allowed_modes = Config.incorporation.allowed_modes
for mode in allowed_modes:
self.incorp_combo.addItem(self.MODE_TEXT[mode], mode)
[docs] def defineMappings(self):
M = self.model_class
if M.incorporation is None:
return []
target = mappers.TargetSpec(
getter=self._currentIncorp,
setter=self._setCurrentIncorp,
signal=self.incorp_combo.currentIndexChanged)
return [(target, M.incorporation)]
def _currentIncorp(self):
return self.incorp_combo.currentData()
def _setCurrentIncorp(self, mode):
for index in range(self.incorp_combo.count()):
item_mode = self.incorp_combo.itemData(index)
if mode == item_mode:
break
else:
index = 0
self.incorp_combo.setCurrentIndex(index)
[docs]class ConfigDialog(ConfigWidgetMixin, basewidgets.BaseOptionsDialog):
ui_module = config_dialog_ui
startRequested = QtCore.pyqtSignal()
[docs] def initSetOptions(self):
super().initSetOptions()
self.help_topic = 'MM_TOPIC_JOB_START_DIALOG'
[docs] def initSetUp(self):
super().initSetUp()
self.incorp_selector = IncorporationSelector()
self._setUpHostSelector()
self._host_layout_widget = HostLayoutWidget(parent=self)
self._host_layout_widget.addHostSelector(self.host_selector)
self.jobname_wdgt = JobnameWidget(parent=self)
self.job_widgets = (self._host_layout_widget, self.jobname_wdgt)
self.reset_btn.hide()
def _setUpHostSelector(self):
self.host_selector = HostSelector(parent=self)
[docs] def initLayOut(self):
super().initLayOut()
self.ui.incorp_layout.addWidget(self.incorp_selector)
self.ui.jobname_layout.addWidget(self.jobname_wdgt)
self.ui.job_group.layout().addWidget(self._host_layout_widget)
def _onConfigClassSet(self):
super()._onConfigClassSet()
M = self.model_class
visible = M.incorporation is not None and bool(maestro)
self.ui.output_group.setVisible(visible)
[docs] def setModel(self, *args, **kwargs):
super().setModel(*args, **kwargs)
self.ui.job_group.setVisible(True)
self.jobname_wdgt.setVisible(self.model_class.jobname is not None)
show_job_group = any([w.isVisibleTo(self) for w in self.job_widgets])
self.ui.job_group.setVisible(show_job_group)
@basewidgets.bottom_button('Run')
def _runBtnSlot(self):
self.accept()
self.startRequested.emit()
[docs] def defineMappings(self):
M = self.model_class
mappings = [(self.host_selector, M.host_settings),
(self.incorp_selector, M), (self.jobname_wdgt, M)]
return mappings