# -*- coding: utf-8 -*-
"""
GUI code for the Desmond panels.
Copyright Schrodinger, LLC. All rights reserved.
"""
import collections
import copy
import future.utils
import os
import random
import shutil
import string
from past.utils import old_div
import schrodinger
import schrodinger.application.desmond.maestro as cmae
import schrodinger.infra.mm as mm
import schrodinger.job.jobcontrol as jobcontrol
import schrodinger.structutils.analyze as analyze
import schrodinger.ui.qt.appframework as af1
import schrodinger.utils.sea as sea
from schrodinger.application.desmond import cmj
from schrodinger.application.desmond import cms
from schrodinger.application.desmond import config
from schrodinger.application.desmond import constants
from schrodinger.application.desmond import cwidget
from schrodinger.application.desmond import desmond_advanced_tab_ui
from schrodinger.application.desmond import envir
from schrodinger.application.desmond import gcmc_utils
from schrodinger.application.desmond import input_group_ui
from schrodinger.application.desmond import license
from schrodinger.application.desmond import platforms
from schrodinger.application.desmond import \
stage # noqa: F401 # yapf: noformat; Required for cmj.msj2sea_full()
from schrodinger.application.desmond import util
from schrodinger.application.matsci import coarsegrain
from schrodinger.application.matsci import msutils
from schrodinger.application.matsci.nano import xtal
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import config_dialog
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import forcefield
from schrodinger.ui.qt import messagebox
from schrodinger.ui.qt.appframework2 import af2
from schrodinger.ui.qt.decorators import wait_cursor
from schrodinger.ui.qt.forcefield import OPLSDirResult
from schrodinger.ui.qt.swidgets import SASLValidator
from schrodinger.ui.qt.swidgets import SLineEdit
from schrodinger.ui.qt.swidgets import SNonNegativeIntValidator
from schrodinger.ui.qt.swidgets import SRealValidator
from schrodinger.ui.qt.swidgets import SNonNegativeRealValidator
from schrodinger.utils import subprocess
try:
from schrodinger.application.desmond.packages import traj
except ImportError:
# Running a unit test w/o desmond being built.
traj = None
maestro = schrodinger.get_maestro()
DES_SCRATCH_ENTRY_NUM_PROP = 'i_des_scratch_entry_num'
DES_SCRATCH_ATOM_NUM_PROP = 'i_des_scratch_atom_num'
DES_FULLSYS_ID_PROP = 'i_des_fullsystem_id'
# TODO: Refactor other repeated class/method/etc. strings per PANEL-8340
NOSE_HOOVER = "Nose-Hoover chain"
DPD = 'DPD'
CPU = "CPU"
GPU = "GPU"
MAX_TIME_TEMP_VALUE = 100000.0
ALLOWED_GROUPNAME_CHARS = string.ascii_letters + string.digits + "_-"
ConfigDialog = config_dialog.ConfigDialog
Host = config_dialog.Host
[docs]def get_msj_template_path(template_fname):
"""
Return a path to the specified MSJ template in the Desmond data directory.
:param template_fname: Template filename to get the path to
:type template_fname: str
:return: Full path to the template location in the Desmond data dir.
:rtype: str
"""
return os.path.join(envir.CONST.MMSHARE_DATA_DESMOND_DIR, template_fname)
[docs]def update_membrane_relaxation_protocol(stage_str):
"""
Update a membrane relaxation protocol based on the current production simulation.
:param stage_str: Current protocol to be updated
:type stage_str: str
:return: Updated stage string with the current production simulation.
:rtype: str
"""
# Remove the last simulate step that was taken from the
# template, as we have added a new production simulate
# stage instead.
stages_map = cmj.msj2sea_full(None, stage_str)
stages_map.stage.pop(-2)
return cmj.write_sea2msj(stages_map.stage)
[docs]def error_dialog(master, msg):
"""
Pops out a dialog showing the error message given by `msg`
This function just provides a uniform way for doing the job.
"""
QtWidgets.QMessageBox.critical(master.app, "Error", msg)
[docs]def warning_dialog(master, msg):
"""
Pops out a dialog showing the warning message given by `msg`
This function just provides a uniform way for doing the job.
"""
mbox = QtWidgets.QMessageBox(master)
mbox.setText("WARNING: %s" % msg)
mbox.setWindowTitle("Warning")
mbox.setIcon(QtWidgets.QMessageBox.Question)
b1 = mbox.addButton("Yes", QtWidgets.QMessageBox.ActionRole)
mbox.addButton("No", QtWidgets.QMessageBox.RejectRole)
mbox.exec()
return (mbox.clickedButton() == b1)
[docs]def get_model_from_pt_row(row):
"""
Check that a specified PT row contains a valid Desmond system and if so
return the system loaded from the row's cms_file attribute.
:param row: Row of the Project Table to process
:type row: `schrodinger.project.ProjectRow`
:return: The `cms.Cms` loaded from the PT's cms_file attribute if possible,
None otherwise.
:rtype: `cms.Cms` or None
"""
# FIXME - This duplicates logic from InputGroup._import_from_workspace_impl
cttype = row[constants.CT_TYPE]
if cttype != constants.CT_TYPE.VAL.FULL_SYSTEM:
return None
cms_file = row.cms_file
if cms_file is None:
return None
try:
model = cms.Cms(cms_file)
except OSError:
return None
if cms.get_model_system_type(
model.comp_ct) == constants.SystemType.ALCHEMICAL:
# This system is prepped for FEP, not Desmond jobs.
return None
return model
[docs]def process_loaded_model(model):
"""
Utility function for processing models getting loaded for various jobs.
Sets full system ID for each atom in the component CTs.
:param model: Model to be processed
:type model: `cms.Cms`
"""
# Prevent traceback for GCMC systems (PANEL-16993), ruins trajectory
# (not used here)
gcmc_utils.remove_inactive_solvent(model)
fullsystem_id = 1
entry_num = 0
for comp_ct in model.comp_ct:
entry_num += 1
for atom in comp_ct.atom:
atom.property[DES_SCRATCH_ENTRY_NUM_PROP] = entry_num
atom.property[DES_SCRATCH_ATOM_NUM_PROP] = atom.index
atom.property[DES_FULLSYS_ID_PROP] = fullsystem_id
# Update the fullsystem atom objects with the entry number and atom index
# as present in the corresponding component ct so that workspace atoms are
# in sync with the atom properties, except for the first component ct as the
# atom from component ct and from fullsystem ct are same for first component ct.
if entry_num > 1:
model.atom[fullsystem_id].property[
DES_SCRATCH_ENTRY_NUM_PROP] = entry_num
model.atom[fullsystem_id].property[
DES_SCRATCH_ATOM_NUM_PROP] = atom.index
fullsystem_id += 1
# Ev:101060 Remove the trajectory property, so that the trajectory
# button does not appear in the Project table entries:
if "s_chorus_trajectory_file" in comp_ct.property:
del comp_ct.property["s_chorus_trajectory_file"]
# FIXME also remove the s_m_original_cms_file property?
[docs]def get_in_cms_from_cpt(cpt_path, cfg=None):
"""
Return the -in.cms file associated with a cpt file.
:param cpt_path: Full path to the cpt file
:type cpt_path: str
:param cfg: Optional config to check. If not specified,
the config will be extracted from the cpt.
:type cfg: `sea.Map` or None
:return: Path to related -in.cms file
:rtype: str
"""
if cfg is None:
cfg = config.extract_cfg(cpt_path)
in_cms_file = os.path.basename(cpt_path[:-4] + "-in.cms")
# get input cms filename from cpt's config.
if cfg:
if 'model_file' in cfg:
in_cms_file = cfg.model_file.val
elif 'replica' in cfg and cfg.replica.val:
in_cms_file = cfg.replica[0].model_file.val
return os.path.join(os.path.dirname(cpt_path), in_cms_file)
[docs]def count_traj_frames(trj_dir):
"""
Count the number of frames in the given trajectory directory. Returns
0 if the trajectory was not readable.
:param trj_dir: Trajectory path
:type: trj_dir: str
:return: Number of frames
:rtype: int
"""
# Value of 0 will mean input is not a (valid) trajectory
if trj_dir is None or not os.path.isdir(trj_dir):
return 0
frames = traj.read_traj(trj_dir)
return len(frames)
[docs]def is_valid_groupname(groupname):
"""
Verify that a given groupname is valid.
:param groupname: Groupname to validate
:type groupname: str
:return: True if the groupname is valid, False otherwise.
:rtype: bool
"""
return all(c in ALLOWED_GROUPNAME_CHARS for c in groupname)
[docs]class HorizontalBar(QtWidgets.QFrame):
[docs] def __init__(self, parent=0):
QtWidgets.QFrame.__init__(self, parent)
self.setFrameShape(QtWidgets.QFrame.HLine)
self.setFrameShadow(QtWidgets.QFrame.Sunken)
[docs]class TempItemDelegate(QtWidgets.QItemDelegate):
"""
Class for allowing the data in the TableModel to be modified.
Only allows float-values to be entered into the table.
"""
[docs] def __init__(self):
QtWidgets.QItemDelegate.__init__(self)
[docs] def createEditor(self, parent, option, index):
editor = QtWidgets.QLineEdit(parent)
editor.setValidator(
QtGui.QDoubleValidator(0.0, MAX_TIME_TEMP_VALUE, 5, parent))
return editor
[docs] def setEditorData(self, editor, index):
value = index.model().data(index, Qt.DisplayRole)
# To prevent the editor from showing ".0" following round values, since
# they aren't shown in the deta cells:
editor.setText("%g" % value)
[docs] def setModelData(self, editor, model, index):
value = editor.text()
model.setData(index, value)
[docs] def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
[docs]class TableModel(QtCore.QAbstractTableModel):
"""
Class for storing the window table information.
"""
[docs] def __init__(self, row_header):
QtCore.QAbstractTableModel.__init__(self)
self.row_header = row_header
self.columns = []
self.is_editable = True
[docs] def setColumns(self, columns):
self.beginResetModel()
self.columns = columns
self.endResetModel()
[docs] def addColumn(self, column):
col_idx = self.columnCount()
self.beginInsertColumns(QtCore.QModelIndex(), col_idx, col_idx)
self.columns.append(column)
self.endInsertColumns()
[docs] def removeColumn(self, col_idx):
self.beginRemoveColumns(QtCore.QModelIndex(), col_idx, col_idx)
self.columns.pop(col_idx)
self.endRemoveColumns()
[docs] def clear(self):
self.setColumns([])
[docs] def update(self):
# FIXME: ideally, panels should change their table model via the
# proper internal methods, and an "update" function like this would not
# be necessary.
self.beginResetModel()
self.endResetModel()
[docs] def rowCount(self, parent=QtCore.QModelIndex()): # noqa: M511
""" Returns number of rows """
return len(self.row_header)
[docs] def columnCount(self, parent=QtCore.QModelIndex()): # noqa: M511
""" Returns number of columns """
return len(self.columns)
[docs] def flags(self, index):
"""
Returns flags for the specified cell. Whether it is a checkbutton
or not.
"""
if not index.isValid():
return Qt.ItemIsSelectable
elif self.is_editable:
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
else:
return Qt.ItemIsSelectable
[docs] def data(self, index, role=Qt.DisplayRole):
"""
Given a cell index, returns the data that should be displayed in that
cell (text or check button state). Used by the view.
"""
if role == Qt.DisplayRole:
window = self.columns[index.column()]
# Need to convert a sea.Atom aboject to a string first:
value = float(str(window[index.row()]))
return round(value, 5)
[docs] def setData(self, index, value, role=Qt.EditRole):
"""
Called by the view to modify the model when the user changes
the data in the table.
"""
if not index.isValid():
return False
window = self.columns[index.column()]
if role == Qt.EditRole:
try:
float_value = float(value)
window[index.row()] = float_value
return True
except ValueError:
# False will be returned for non-float value (like "")
return False
[docs]class PosResDelegate(QtWidgets.QItemDelegate):
"""
Class for allowing the data in the PosResModel to be modified
"""
[docs] def __init__(self):
QtWidgets.QItemDelegate.__init__(self)
[docs] def createEditor(self, parent, option, index):
editor = QtWidgets.QLineEdit(parent)
if index.column() == 0:
# Force constant
editor.setValidator(QtGui.QDoubleValidator(parent))
return editor
[docs] def setEditorData(self, editor, index):
value = index.model().data(index, Qt.DisplayRole)
editor.setText(str(value))
[docs] def setModelData(self, editor, model, index):
if index.column() == 0:
value = editor.text()
try:
float_value = float(value)
except:
return # FIXME return error?
else:
model.setData(index, float_value)
elif index.column() == 1:
value = str(editor.text())
# FIXME make sure this is a valid ASL?
model.setData(index, value)
[docs] def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
class _BaseAslRow(object):
"""
Base class for PosResRow and AtomGroupRow.
"""
def __init__(self, panel):
"""
:param af2_app Pass the guiapp instance. This is required when the
panel is running outside of Maestro, to call gerWorkspaceStructure()
"""
self.asl = ""
self.atoms = set()
self._panel = panel
def validate(self):
if not self.asl:
return "No ASL specified"
if len(self.atoms) == 0:
if not analyze.validate_asl(self.asl):
return "Specified ASL is invalid"
else:
return "ASL did not match any Workspace atoms"
return None
def setAsl(self, asl):
"""
Set the ASL of the row object to the given value.
If ASL is valid, sets the <atoms> list to the matching Workspace atoms.
If ASL is invalid, raises ValueError and sets <atoms> to empty set.
Empty ASL ("") sets <atoms> to an empty set (row is invalid).
"""
self.asl = asl
if asl:
if not analyze.validate_asl(asl):
raise ValueError
# TODO: Ideally the ASL should be evaluated only when the dialog is
# closed/applied. Currently, if the user has the wrong structure
# in the Workspace (causing the ASL to not match any atoms), they
# have to edit the row's ASL after modifying the Workspace, to get
# the ASL re-evaluated.
st = self._panel.getWorkspaceStructure()
atoms = analyze.evaluate_asl(st, asl)
atom_set = set()
for idx in atoms:
atom = st.atom[idx]
entry_num = atom.property.get(DES_SCRATCH_ENTRY_NUM_PROP)
number_by_entry = atom.property.get(DES_SCRATCH_ATOM_NUM_PROP)
atom_set.add((entry_num, number_by_entry))
self.atoms = atom_set
else:
self.atoms = set()
[docs]class PosResRow(_BaseAslRow):
[docs] def __init__(self, panel):
"""
:param guiapp Pass the guiapp instance.
"""
super(PosResRow, self).__init__(panel)
# DESMOND-2342 Default force constant:
self.force_constant = 1.0
[docs] def validate(self):
"""
Returns None if all values of this row are valid; error message
otherwise.
"""
if self.force_constant <= 0:
return "Invalid force constant specified"
return super(PosResRow, self).validate()
[docs]class PosResModel(QtCore.QAbstractTableModel):
"""
Class for storing the window table information.
"""
warning = QtCore.pyqtSignal(str)
[docs] def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.header = ["Force Constant", "Atoms (ASL)", "# of atoms"]
self.header_tooltips = [u"Units kcal/mol/Ų", None, None]
self.rows = []
[docs] def setRows(self, rows):
self.beginResetModel()
self.rows = rows
self.endResetModel()
[docs] def clear(self):
self.setRows([])
[docs] def addOneRow(self, row):
row_idx = self.rowCount()
self.beginInsertRows(QtCore.QModelIndex(), row_idx, row_idx)
self.rows.append(row)
self.endInsertRows()
[docs] def removeOneRow(self, rownum):
self.beginRemoveRows(QtCore.QModelIndex(), rownum, rownum)
self.rows.pop(rownum)
self.endRemoveRows()
[docs] def rowCount(self, parent=QtCore.QModelIndex()): # noqa: M511
""" Returns number of rows """
return len(self.rows)
[docs] def columnCount(self, parent=QtCore.QModelIndex()): # noqa: M511
""" Returns number of columns """
return len(self.header)
[docs] def flags(self, index):
"""
Returns flags for the specified cell. Whether it is a checkbutton
or not.
"""
if not index.isValid():
return Qt.ItemIsSelectable
else:
if index.column() in [0, 1]:
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | \
Qt.ItemIsEditable
else:
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
[docs] def data(self, index, role=Qt.DisplayRole):
"""
Given a cell index, returns the data that should be displayed in that
cell (text or check button state). Used by the view.
"""
if role == Qt.DisplayRole:
coli = index.column()
row = self.rows[index.row()]
if coli == 0:
return row.force_constant
elif coli == 1:
return row.asl
elif coli == 2:
return len(row.atoms)
[docs] def setData(self, index, value, role=Qt.EditRole):
"""
Called by the view to modify the model when the user changes
the data in the table.
"""
if not index.isValid():
return False
coli = index.column()
row = self.rows[index.row()]
if role == Qt.EditRole:
if coli == 0:
row.force_constant = float(value)
elif coli == 1:
try:
row.setAsl(value)
except ValueError:
self.warning.emit("Invalid ASL specified")
# Still change the value in the cell, as overwriting user's
# input will force them to re-type it in. Note that the row
# object is invalid if setAsl() raises ValueError.
return True
return False
class _MdcBase(object):
"""
The Advanced options dialogs and their tab widgets subclass this class.
"""
def __init__(self, widgets):
self.refresh(widgets)
def refresh(self, widgets):
self._widgets = widgets
def add(self, w):
if (w.__class__ is list):
self._widgets.extend(w)
else:
self._widgets.append(w)
def remove(self, w):
if (w.__class__ is list):
for w_ in w:
try:
self._widgets.remove(w_)
except ValueError:
pass
else:
try:
self._widgets.remove(w)
except ValueError:
pass
def hasWidget(self, w):
return w in self._widgets
def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
for w in self._widgets:
out = w.checkValidity()
if out:
return out
return None
def updateKey(self, key):
# _MdcBase
for w in self._widgets:
w.updateKey(key)
def updateFromKey(self, key):
for w in self._widgets:
w.updateFromKey(key)
def resetFromModel(self, model):
pass
def updateModel(self, model):
pass
[docs]class IntegrationTab(_MdcBase):
"""
Frame for the Integration tab of the Mdc Advanced Options dialogs
"""
[docs] def __init__(self, master, key, ui):
self._master = master
self._ui = ui
self.respa_bond_ef = self._ui.respa_bond_ef
self.respa_bond_ef.textEdited.connect(self.respaBondChanged)
self.respa_bond_ef.setValidator(
QtGui.QDoubleValidator(0.0000001, 100000000.0, 5, self._master))
self.respa_near_spinbox = self._ui.respa_near_spinbox
self.respa_far_spinbox = self._ui.respa_far_spinbox
_MdcBase.__init__(self, [])
# __init__
[docs] def respaBondChanged(self):
try:
new_dt = float(self.respa_bond_ef.text())
if new_dt == 0.0:
return
old_dt = self.respa_near_spinbox.singleStep()
near = self.respa_near_spinbox.value() / old_dt * new_dt
far = self.respa_far_spinbox.value() / old_dt * new_dt
self.respa_near_spinbox.setRange(new_dt, new_dt * 7)
self.respa_near_spinbox.setSingleStep(new_dt)
self.respa_near_spinbox.setValue(near)
self.respa_far_spinbox.setRange(new_dt, new_dt * 14)
self.respa_far_spinbox.setSingleStep(new_dt)
self.respa_far_spinbox.setValue(far)
except ValueError:
pass
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
self.respaBondChanged()
near_dt = self.respa_near_spinbox.value()
far_dt = float(self.respa_far_spinbox.value())
if (near_dt > far_dt):
return "Value of far spinbox is less than value of the near spinbox"
if not self.respa_bond_ef.hasAcceptableInput():
return "Invalid value for respa_bond_ef field: '%s'" % \
self.respa_bond_ef.text()
return None
[docs] def updateKey(self, key):
# IntegrationTab
key.timestep[0].val = old_div(float(self.respa_bond_ef.text()), 1000.0)
key.timestep[1].val = old_div(self.respa_near_spinbox.value(), 1000.0)
key.timestep[2].val = old_div(self.respa_far_spinbox.value(), 1000.0)
[docs] def updateFromKey(self, key):
def updateTimestepSpinbox(bondval, newval, box):
"""
Update the spinbox parameters
:type bondval: float
:param bondval: The value from the bond timestep
:type newval: float
:param newval: The new value for the spinbox
:type box: QDoubleSpinBox
:param box: The spinbox to set the parameters for
"""
# Ensure the min and max can accomodate the new value
box.setMinimum(min(newval, box.minimum()))
box.setMaximum(max(newval, box.maximum()))
box.setSingleStep(bondval)
box.setValue(newval)
# Converts from ps to fs.
timestep = [e * 1000 for e in key.timestep.val]
self.respa_bond_ef.setText(str(timestep[0]))
updateTimestepSpinbox(timestep[0], timestep[1], self.respa_near_spinbox)
updateTimestepSpinbox(timestep[0], timestep[2], self.respa_far_spinbox)
self.respaBondChanged()
[docs]class EnsembleTab(_MdcBase):
"""
Frame for the Ensemble tab of the Advanced Options dialog.
"""
STYLE_NAMEMAP = {
"isotropic": "Isotropic",
"semi_isotropic": "Semi-isotropic",
"anisotropic": "Anisotropic",
"constant_area": "Constant area",
"Isotropic": "isotropic",
"Semi-isotropic": "semi_isotropic",
"Anisotropic": "anisotropic",
"Constant area": "constant_area",
}
METHOD_NAMEMAP = {
"MTK": NOSE_HOOVER,
"NH": NOSE_HOOVER,
"Langevin": "Langevin",
DPD: DPD,
}
[docs] def __init__(self, master, key, is_annealing, ui):
self._master = master
self._ui = ui
self._old_baro_method = None
# Whether we are currently in one of the *MethodChanged() methods.
self._in_thermo_method_changed = False
self._in_baro_method_changed = False
self.thermo_method_menu = self._ui.thermo_method_menu
self.thermo_method_menu.currentIndexChanged.connect(
self.thermoMethodChanged)
self.is_annealing = is_annealing
if is_annealing:
self.thermo_method_menu.removeItem(
self.thermo_method_menu.findText("None"))
self.ngroups_label = self._ui.ngroups_label
self.ngroups_spinbox = self._ui.ngroups_spinbox
self.ngroups_spinbox.valueChanged.connect(self.numGroupsChanged)
self.temp_model = TableModel(["Temperature (K)"])
self.temp_view = self._ui.temp_view
self.temp_view.setModel(self.temp_model)
self.temp_view.horizontalHeader().setDefaultSectionSize(50)
if is_annealing:
self.ngroups_spinbox.hide()
self.temp_view.hide()
self.ngroups_label.hide()
self.therm_tau_ef = self._ui.therm_tau_ef
self.therm_tau_ef.setValidator(
QtGui.QDoubleValidator(0.0, 1000000000000.0, 10, self._master))
self._method_map = {
"MTK_NPT": "Martyna-Tobias-Klein",
"L_NPT": "Langevin",
}
self._styles = (
"Isotropic",
"Semi-isotropic",
"Anisotropic",
"Constant area",
)
self.baro_method_menu = self._ui.baro_method_menu
self.baro_method_menu.currentIndexChanged.connect(
self.baroMethodChanged)
self.baro_tau_ef = self._ui.baro_tau_ef
self.baro_tau_ef.setValidator(
QtGui.QDoubleValidator(0.0, 10000000000, 100, self._master))
self.baro_style_label = self._ui.baro_style_label
self.baro_style_menu = self._ui.baro_style_menu
_MdcBase.__init__(self, [])
# __init__
[docs] def getBaroMethod(self):
return str(self.baro_method_menu.currentText())
[docs] def setBaroMethod(self, method):
index = self.baro_method_menu.findText(method)
self.baro_method_menu.setCurrentIndex(index)
[docs] def getThermoMethod(self):
return str(self.thermo_method_menu.currentText())
[docs] def setThermoMethod(self, method):
index = self.thermo_method_menu.findText(method)
self.thermo_method_menu.setCurrentIndex(index)
[docs] def baroMethodChanged(self, index):
method = self.baro_method_menu.itemText(index)
if self._in_baro_method_changed:
return
self._in_baro_method_changed = True
method = str(method)
enable = (method != "None")
self.baro_tau_ef.setEnabled(enable)
self.baro_style_label.setEnabled(enable)
self.baro_style_menu.setEnabled(enable)
if method == "Langevin":
self.setThermoMethod("Langevin")
elif method == "Martyna-Tobias-Klein":
if self.getThermoMethod() != DPD:
self.setThermoMethod(NOSE_HOOVER)
self._old_baro_method = method
self._in_baro_method_changed = False
[docs] def thermoMethodChanged(self, index):
"""
"""
method = self.thermo_method_menu.currentText()
if self._in_thermo_method_changed:
return
self._in_thermo_method_changed = True
method = str(method)
enable = (method != "None")
if not self.is_annealing:
self.ngroups_spinbox.setEnabled(enable)
self.temp_view.setEnabled(enable)
self.therm_tau_ef.setEnabled(enable)
if (method == "None"):
self._old_baro_method = "None"
self.setBaroMethod("None")
elif method == "Langevin" and self._old_baro_method != "None":
self._old_baro_method = "Langevin"
self.setBaroMethod("Langevin")
elif method in (NOSE_HOOVER, DPD) and self._old_baro_method != "None":
self._old_baro_method = "Martyna-Tobias-Klein"
self.setBaroMethod("Martyna-Tobias-Klein")
self._in_thermo_method_changed = False
[docs] def setBaroStyle(self, style):
index = self.baro_style_menu.findText(style)
self.baro_style_menu.setCurrentIndex(index)
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
method = str(self.baro_method_menu.currentText())
if method != "None":
if not self.baro_tau_ef.hasAcceptableInput():
return "Invalid value for baro_tau_ef: '%s'" % \
self.baro_tau_ef.text()
if not self.therm_tau_ef.hasAcceptableInput():
return "Invalid value: '%s'" % self.therm_tau_ef.text()
method = str(self.thermo_method_menu.currentText())
if method != "None":
if not self.is_annealing:
self.numGroupsChanged()
if not self.ngroups_spinbox.hasAcceptableInput():
return "Invalid value for ngroups_spinbox: '%s'" % \
self.ngroups_spinbox.text()
# FIXME make sure cells of self.temp_model have valid values
return None
[docs] def updateKey(self, key):
# EnsembleTab
baro_method = str(self.baro_method_menu.currentText())
baro_style = str(self.baro_style_menu.currentText())
thermo_method = str(self.thermo_method_menu.currentText())
if thermo_method == "None":
# Both thermo and bar are None
key.ensemble.class_.val = "NVE"
else:
# Thermostat is on
if thermo_method == NOSE_HOOVER:
thermo_method = "NH"
key.ensemble.method.val = thermo_method
if baro_method == "None":
# Thremostat is on but barstat is off:
key.ensemble.class_.val = "NVT"
else:
if baro_method == "Martyna-Tobias-Klein":
# overwrites previously set method.val
if thermo_method == DPD:
key.ensemble.method.val = DPD
else:
key.ensemble.method.val = "MTK"
if baro_style == "Semi-isotropic":
key.ensemble.class_.val = "NPgT"
elif baro_style == "Constant area":
key.ensemble.class_.val = "NPAT"
else:
key.ensemble.class_.val = "NPT"
key.pressure[1].val = EnsembleTab.STYLE_NAMEMAP[baro_style]
key.ensemble.barostat.tau.val = str(self.baro_tau_ef.text())
key.ensemble.thermostat.tau.val = str(self.therm_tau_ef.text())
if not self.is_annealing:
temp_list = []
for i in range(self.temp_model.columnCount()):
atom_obj = self.temp_model.columns[i][0]
temp_list.append([
atom_obj,
i,
])
key["temperature"] = sea.List(temp_list)
[docs] def updateFromKey(self, key):
if key.ensemble.class_.val == "NPgT":
initial_style = "semi_isotropic"
elif key.ensemble.class_.val == "NPAT":
initial_style = "constant_area"
else:
initial_style = key.pressure[1].val
initial_style = self.STYLE_NAMEMAP[initial_style]
for i, stylename in enumerate(self._styles):
if stylename == initial_style:
self.baro_style_menu.setCurrentIndex(i)
initial_tau = key.ensemble.barostat.tau.val
self.baro_tau_ef.setText(str(initial_tau))
# Initial barostat method
initial_baro_method = key.ensemble.method.val
if initial_baro_method in {"MTK", "NH", DPD}:
initial_baro_method = "Martyna-Tobias-Klein"
ens_class = key.ensemble.class_.val
if ens_class in ["NVE", "NVT"]:
initial_baro_method = "None"
# Berendsen is not supported, and backward compatibility is supported by
# replacing it with Langevin
baro_txt = initial_baro_method if initial_baro_method != 'Berendsen' else 'Langevin'
index = self.baro_method_menu.findText(baro_txt)
if index == -1:
raise ValueError("Menu text not found: %s" % initial_baro_method)
self.baro_method_menu.setCurrentIndex(index)
# Initial thermostate method
if key.ensemble.class_.val == "NVE":
initial_thermo_method = "None"
elif key.ensemble.method.val == 'Berendsen':
initial_thermo_method = self.METHOD_NAMEMAP['Langevin']
else:
initial_thermo_method = self.METHOD_NAMEMAP[key.ensemble.method.val]
# FIXME is this correct?
if self.is_annealing and initial_thermo_method == "None":
initial_thermo_method = NOSE_HOOVER
index = self.thermo_method_menu.findText(initial_thermo_method)
self.thermo_method_menu.setCurrentIndex(index)
if not self.is_annealing:
# Populate the temperature table:
self.temp_model.clear()
for t, g in key.temperature:
column_data = [t]
self.temp_model.addColumn(column_data)
self.ngroups_spinbox.setValue(self.temp_model.columnCount())
self.therm_tau_ef.setText(str(key.ensemble.thermostat.tau.val))
# This is to avoid error printouts that happen if there is no model in
# the Workspace:
if not self.is_annealing:
if len(self.temp_model.columns) == 0:
self.temp_model.addColumn([0.0])
[docs] def resetEnsClass(self, ens_class):
dpd = self._ui.thermo_method_menu.findText(DPD)
dpd_item = self._ui.thermo_method_menu.model().item(dpd)
dpd_item.setEnabled(False) # default no DPD choice
if (ens_class == "NVE"):
baro_method = "None"
self.setThermoMethod("None")
elif (ens_class == "NVT"):
dpd_item.setEnabled(True) # only DPD for NVT and NPT
baro_method = "None"
if (self.getThermoMethod() == "None"):
self.setThermoMethod(NOSE_HOOVER)
elif (self.getBaroMethod() != "None"):
self.setBaroMethod("None")
elif (ens_class == "NPAT"):
baro_method = "Martyna-Tobias-Klein"
self.setBaroMethod(baro_method)
self.setBaroStyle("Constant area")
elif ens_class == u"NP\u03B3T":
baro_method = "Martyna-Tobias-Klein"
self.setBaroMethod(baro_method)
self.setBaroStyle("Semi-isotropic")
else:
dpd_item.setEnabled(True) # only DPD for NVT and NPT
if (self.getBaroMethod() == "None"):
if (self.getThermoMethod() == "Langevin"):
baro_method = "Langevin"
else:
baro_method = "Martyna-Tobias-Klein"
self.setBaroMethod(baro_method)
else:
baro_method = self.getBaroMethod()
self.setBaroStyle("Isotropic")
self._old_baro_method = baro_method
[docs] def numGroupsChanged(self, ignored=None):
"""
"""
if not self.ngroups_spinbox.hasAcceptableInput():
QtWidgets.QMessageBox.critical(self, "Error",
"Number of groups is invalid")
self.ngroups_spinbox.setFocus()
return
n_group = self.ngroups_spinbox.value()
n_group_old = self.temp_model.columnCount()
t_ref = self.temp_model.columns[n_group_old - 1][0]
if (n_group > n_group_old):
for i in range(n_group_old, n_group):
self.temp_model.addColumn([t_ref])
elif (n_group < n_group_old):
for i in range(n_group, n_group_old):
self.temp_model.removeColumn(i - 1)
[docs] def resetThermoTemp(self, t):
n_group = self.ngroups_spinbox.value()
for i in range(n_group):
try:
self.temp_model.columns[i][0] = t
except IndexError:
self.temp_model.addColumn([t])
# Resets temperature for the barostat degree of freedom.
self.temp_model.columns[-1][0] = t
[docs]class InteractionTab(_MdcBase):
"""
Frame for the Interaction frame of the Advanced Options dialog.
"""
NEAR_METHOD_NAMEMAP = {
"c2switch": "Force tapering",
"c1switch": "Potential tapering",
"none": "Cutoff",
"shift": "Shift",
"Force tapering": "c2switch",
"Potential tapering": "c1switch",
"Cutoff": "none",
"Shift": "shift",
}
[docs] def __init__(self, master, key, ui, command=None):
self._master = master
self._ui = ui
self._command = command
self._max_cut = 30.0
self._min_cut = 1.2
self.near_method_menu = self._ui.near_method_menu
self.coul_cutoff_label = self._ui.coul_cutoff_label
self.coul_cutoff_ef = self._ui.coul_cutoff_ef
self.coul_cutoff_ef.setValidator(
QtGui.QDoubleValidator(self._min_cut, self._max_cut, 1000,
self._master))
self.tapering_label_1 = self._ui.tapering_label_1
self.tapering_label_2 = self._ui.tapering_label_2
self.tapering_label_3 = self._ui.tapering_label_3
self.tapering_from_ef = self._ui.tapering_from_ef
self.tapering_from_ef.textEdited.connect(self.taperingModified)
self.tapering_from_ef.setValidator(
QtGui.QDoubleValidator(self._min_cut, self._max_cut, 1000,
self._master))
self.tapering_to_ef = self._ui.tapering_to_ef
self.tapering_to_ef.textEdited.connect(self.taperingModified)
self.tapering_to_ef.setValidator(
QtGui.QDoubleValidator(self._min_cut, self._max_cut, 1000,
self._master))
self.near_method_menu.currentIndexChanged.connect(
self.nearMethodChanged)
# Show/hide widgets:
self.nearMethodChanged()
_MdcBase.__init__(self, [])
[docs] def nearMethodChanged(self, index=None):
method = self.near_method_menu.currentText()
if str(method) == "Cutoff":
self.coul_cutoff_ef.show()
self.coul_cutoff_label.show()
self.tapering_label_1.hide()
self.tapering_label_2.hide()
self.tapering_label_3.hide()
self.tapering_from_ef.hide()
self.tapering_to_ef.hide()
else:
self.coul_cutoff_ef.hide()
self.coul_cutoff_label.hide()
self.tapering_label_1.show()
self.tapering_label_2.show()
self.tapering_label_3.show()
self.tapering_from_ef.show()
self.tapering_to_ef.show()
[docs] def taperingModified(self):
is_from_valid = self.tapering_from_ef.hasAcceptableInput()
is_to_valid = self.tapering_to_ef.hasAcceptableInput()
if (is_from_valid):
from_ = float(self.tapering_from_ef.text())
if (is_to_valid):
to = float(self.tapering_to_ef.text())
if (is_from_valid and is_to_valid and from_ > to):
pass
[docs] def resetFromModel(self, model=None):
"""
Update the interaction tab UI based on the given model CTs.
"""
if model:
box_x, box_y, box_z = cms.get_boxsize(model.box)
max_cut = min(box_x * 0.5, box_y * 0.5, box_z * 0.5)
else:
max_cut = 30.0
self._max_cut = min(30.0, max_cut)
self.coul_cutoff_ef.setValidator(
QtGui.QDoubleValidator(self._min_cut, self._max_cut, 1000,
self._master))
self.tapering_from_ef.setValidator(
QtGui.QDoubleValidator(self._min_cut, self._max_cut, 1000,
self._master))
self.tapering_to_ef.setValidator(
QtGui.QDoubleValidator(self._min_cut, self._max_cut, 1000,
self._master))
if self._max_cut < self._master._key.cutoff_radius.val:
self._master._key.cutoff_radius.val = self._max_cut
self.coul_cutoff_ef.setText(str(self._master._key.cutoff_radius.val))
self._ui.coul_cutoff_ef.setText(str(
self._master._key.cutoff_radius.val))
[docs] def updateModel(self, model):
pass
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
near_method = str(self.near_method_menu.currentText())
if near_method == "Cutoff":
if not self.coul_cutoff_ef.hasAcceptableInput():
return "Invalid value for Cuttoff Radius: '%s'" % \
self.coul_cutoff_ef.text()
else:
if not self.tapering_from_ef.hasAcceptableInput():
return "Invalid value for Tapering range from: '%s'" % \
self.tapering_from_ef.text()
if not self.tapering_to_ef.hasAcceptableInput():
return "Invalid value for Tapering range to: '%s'" % \
self.tapering_to_ef.text()
from_ = float(self.tapering_from_ef.text())
to = float(self.tapering_to_ef.text())
if from_ > to:
return "Tapering to is less than from"
return None
[docs] def updateKey(self, key):
# InteractionTab
near_method = str(self.near_method_menu.currentText())
if (near_method == "Cutoff"):
key["taper"] = False
key.cutoff_radius.val = float(self.coul_cutoff_ef.text())
else:
near_method = self.NEAR_METHOD_NAMEMAP[near_method]
r_cut = float(self.tapering_to_ef.text())
r_tap = float(self.tapering_from_ef.text())
key["taper"] = sea.Map("method = %s width = %f" % (
near_method,
r_cut - r_tap,
))
key.cutoff_radius.val = r_cut
[docs] def updateFromKey(self, key):
taper_from = key.cutoff_radius.val
taper_to = taper_from
if (isinstance(key.taper, sea.Atom)):
near_method = "Cutoff"
else:
near_method = self.NEAR_METHOD_NAMEMAP[key.taper.method.val]
taper_from = taper_from - key.taper.width.val
index = self.near_method_menu.findText(near_method)
self.near_method_menu.setCurrentIndex(index)
self.coul_cutoff_ef.setText(str(taper_to))
self.tapering_from_ef.setText(str(taper_from))
self.tapering_to_ef.setText(str(taper_to))
[docs]class RestraintsTab(_MdcBase):
"""
Frame for the Restraints tab of the Advanced Options dialog
"""
[docs] def __init__(self, master, key, ui):
self._master = master
self._ui = ui
self.posres_model = PosResModel()
self.posres_model.warning.connect(
lambda msg: QtWidgets.QMessageBox.warning(master, "Warning", msg))
self.posres_view = self._ui.posres_view
self.posres_view.setModel(self.posres_model)
self.posres_view.setItemDelegate(PosResDelegate())
self.posres_view.setSelectionBehavior(
QtWidgets.QAbstractItemView.SelectRows)
self.posres_view.selectionModel().selectionChanged.connect(
self.updatePosresButtons)
self.posres_model.layoutChanged.connect(self.updatePosresButtons)
self.posres_model.modelReset.connect(self.updatePosresButtons)
self.posres_model.rowsInserted.connect(
lambda: self.updatePosresButtons())
self.posres_model.columnsInserted.connect(
lambda: self.updatePosresButtons())
self.posres_view.setColumnWidth(0, 120)
self.posres_view.setColumnWidth(1, 220)
self.posres_view.setColumnWidth(2, 120)
self.btn_select = self._ui.btn_select
self.btn_select.clicked.connect(self.selectPosResAsl)
self.btn_add = self._ui.btn_add
self.btn_add.clicked.connect(self.addPosResRow)
self.btn_delete = self._ui.btn_delete
self.btn_delete.clicked.connect(self.deletePosResRow)
self.btn_reset = self._ui.btn_reset
self.btn_reset.clicked.connect(self.resetPosResTable)
self.cpt_info = self._ui.cpt_info
self.cpt_info.setVisible(False)
self.addPosResRow()
self.resetPosResTable()
self.updatePosresButtons()
_MdcBase.__init__(self, [])
# __init__
[docs] def selectPosResAsl(self):
irow = self.posres_view.selectionModel().selectedRows()[0].row()
row = self.posres_model.rows[irow]
title = "Desmond - Select atoms for position-restraint group"
new_asl = maestro.atom_selection_dialog(title, row.asl)
if new_asl:
row.setAsl(new_asl)
# Redraw the visible cells with new data:
self.posres_view.viewport().update()
[docs] def addPosResRow(self):
# Pass the GuiApp instance to PosResRow.
row = PosResRow(self._master)
self.posres_model.addOneRow(row)
[docs] def deletePosResRow(self):
irow = self.posres_view.selectionModel().selectedRows()[0].row()
self.posres_model.removeOneRow(irow)
self.updatePosresButtons()
[docs] def resetPosResTable(self):
self.posres_model.clear()
self.updatePosresButtons()
[docs] def getRestrGroup(self):
posres_groups = []
for i_row, row in enumerate(self.posres_model.rows, start=1):
err = row.validate()
if not err:
try:
k = row.force_constant
grp = _AtomGroup(float(k))
grp.atom[0] = row.atoms
grp.size[0] = len(row.atoms)
grp.asl[0] = row.asl
posres_groups.append(grp)
except AttributeError:
# No atom is selected.
raise # FIXME
# pass
else:
raise Exception(
"Invalid entry in position-restraint table for row %i\n%s" %
(i_row, err))
return posres_groups
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
# TODO: ASL evaluation needs to be done at the time when the
# dialog is validated/closed instead of when the row's ASL
# field is edited.
for i_row, row in enumerate(self.posres_model.rows, start=1):
err = row.validate()
if err:
return "Invalid data in row %s of the position restraint table:\n%s" % (
i_row, err)
return None
[docs] def resetFromModel(self, model):
"""
Update the Restraints tab UI based on the given model CTs.
"""
if model:
restr_grp = _AtomGroup.get_restr(model.comp_ct)
# Saves the original restraints so that we can compare
# new settings against them
#model.posre = restr_grp
self.posres_model.clear()
for g in restr_grp:
row = PosResRow(self._master)
row.atoms = g.atom[0]
row.name = g.name
row.asl = g.asl[0]
self.posres_model.addOneRow(row)
self.cpt_info.setVisible(False)
else:
self._ui.groupBox_15.setEnabled(False)
self.cpt_info.setVisible(True)
[docs] def updateModel(self, model):
"""
Updates the given model to the data from the UI
"""
posre = self.getRestrGroup()
_AtomGroup.inject_restr(model.comp_ct, posre)
[docs] def updateKey(self, key):
# RestraintsTab
# Not implemented, because the restraint data is stored in the model
# CT and not in the key.
return 0
[docs] def updateFromKey(self, key):
"""
Update the options in the restraints tab to the model.
"""
# Not implemented, because the restraint data is stored in the model
# CT and not in the key.
return
[docs] def reset(self):
"""
Called when the panel is reset.
"""
self.resetPosResTable()
[docs]class OutputTab(_MdcBase):
"""
Frame for the Output tab of the Advanced Options dialog
"""
[docs] def __init__(self, master, key, ui):
self._master = master
self._ui = ui
self.energy_file_ef = self._ui.energy_file_ef
self.energy_start_ef = self._ui.energy_start_ef
self.energy_start_ef.setValidator(
QtGui.QDoubleValidator(0.0, 1000000000, 10, self._master))
self.traj_name_ef = self._ui.traj_name_ef
self.traj_start_ef = self._ui.traj_start_ef
self.traj_start_ef.setValidator(
QtGui.QDoubleValidator(0.0, 1000000000, 10, self._master))
self.traj_vel_box = self._ui.traj_vel_box
self.traj_center_asl_fe = self._ui.traj_center_asl_fe
self.traj_center_asl_fe.setValidator(SASLValidator())
self._ui.traj_center_asl_btn.clicked.connect(self.setASL)
self.traj_glue_box = self._ui.traj_glue_box
self.checkpt_file_ef = self._ui.checkpt_file_ef
self.checkpt_start_ef = self._ui.checkpt_start_ef
self.checkpt_start_ef.setValidator(
QtGui.QDoubleValidator(0.0, 1000000000, 10, self._master))
self.checkpt_inter_ef = self._ui.checkpt_inter_ef
self.checkpt_inter_ef.setValidator(
QtGui.QDoubleValidator(0.0, 1000000000, 10, self._master))
self.updateFromKey(key)
_MdcBase.__init__(self, [])
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
for_validation = [
self.energy_file_ef,
self.energy_start_ef,
self.traj_name_ef,
self.traj_start_ef,
self.traj_center_asl_fe,
self.checkpt_file_ef,
self.checkpt_start_ef,
self.checkpt_inter_ef,
]
for item in for_validation:
if not item.hasAcceptableInput():
return "Invalid value: '%s'" % item.text()
return None
[docs] def updateKey(self, key):
# OutputTab
key.eneseq.name.val = str(self.energy_file_ef.text())
key.eneseq.first.val = str(self.energy_start_ef.text())
key.trajectory.name.val = str(self.traj_name_ef.text())
key.trajectory.first.val = str(self.traj_start_ef.text())
key.trajectory.write_velocity.val = self.traj_vel_box.isChecked()
# By default, center is an empty list. Keep it as is if ASL wasn't set,
# otherwise, change sea type to str instead of a list
key.trajectory['center'] = str(self.traj_center_asl_fe.text()) or []
key.checkpt.name.val = str(self.checkpt_file_ef.text())
key.checkpt.first.val = str(self.checkpt_start_ef.text())
key.checkpt.interval.val = str(self.checkpt_inter_ef.text())
key.glue.val = "solute" if (self.traj_glue_box.isChecked()) else "none"
[docs] def updateFromKey(self, key):
def get_trj_center_asl(key):
# If center is a list (default value is []), return empty string
if isinstance(key.trajectory.center.val, list):
return ''
else:
return str(key.trajectory.center.val)
self.energy_file_ef.setText(key.eneseq.name.raw_val)
self.energy_start_ef.setText(str(key.eneseq.first.val))
self.traj_name_ef.setText(key.trajectory.name.raw_val)
self.traj_start_ef.setText(str(key.trajectory.first.val))
self.traj_vel_box.setChecked(key.trajectory.write_velocity.val)
self.traj_center_asl_fe.setText(get_trj_center_asl(key))
self.checkpt_file_ef.setText(key.checkpt.name.raw_val)
self.checkpt_start_ef.setText(str(key.checkpt.first.val))
self.checkpt_inter_ef.setText(str(key.checkpt.interval.val))
self.traj_glue_box.setChecked(key.glue.val == "solute")
[docs] def setASL(self):
title = "Desmond - Define trajectory centering"
new_asl = maestro.atom_selection_dialog(title,
self.traj_center_asl_fe.text())
if new_asl:
self.traj_center_asl_fe.setText(new_asl)
[docs]class RandVelFrame(_MdcBase):
"""
Frame for the "Randomize velocities" group of the Misc tab of Advanced
dialog.
"""
[docs] def __init__(self, master, key, ui, root_master):
self._master = master
self._ui = ui
self._setupRandomSeed()
self.first_ef = self._ui.first_ef
self.first_ef.setValidator(
QtGui.QDoubleValidator(0, 100000000, 10, root_master))
self.interval_ef = self._ui.interval_ef
self.interval_ef.setValidator(
QtGui.QDoubleValidator(0.0, 10000000, 10, root_master))
self.updateFromKey(key)
_MdcBase.__init__(self, [])
def _setupRandomSeed(self):
"""
Sets up random seed related widgets.
"""
seed_label_tooltip = ("Using different random seeds in two simulations"
" will result in two distinct simulation paths.")
self._ui.seed_label.setToolTip(seed_label_tooltip)
self._ui.seed_button_group.buttonClicked.connect(self.enableCustomSeed)
[docs] def enableCustomSeed(self):
"""
Enables or disables custom seed line edit based on the radio buttons.
"""
enable = self._ui.custom_seed_rb.isChecked()
self._ui.custom_seed_sb.setEnabled(enable)
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
if not self.first_ef.hasAcceptableInput():
return "Invalid value for first_ef field: '%s'" % \
self.first_ef.text()
if not self.interval_ef.hasAcceptableInput():
return "Invalid value for interval_ef field: '%s'" % \
self.interval_ef.text()
return None
[docs] def updateKey(self, key):
# RandVelFrame
# key.randomize_velocity.seed type is "int", see config.py
if self._ui.custom_seed_rb.isChecked():
seed = self._ui.custom_seed_sb.value()
if not seed:
seed = 2007
else:
# when generating a random seed, keep it under 10000.
seed = random.randint(1, 9999)
if self._ui.rand_gb.isChecked():
first = str(self.first_ef.text())
interval = str(float(self.interval_ef.text()))
if interval == "0.0":
interval = "inf"
else:
first = "inf"
interval = "inf"
key.randomize_velocity.seed.val = seed
key.randomize_velocity.first.val = first
key.randomize_velocity.interval.val = interval
[docs] def updateFromKey(self, key):
seed = key.randomize_velocity.seed.val # int
first = str(key.randomize_velocity.first.val)
interval = str(key.randomize_velocity.interval.val)
self._ui.rand_gb.setChecked(first != 'inf')
if first == "inf":
first = "0.0"
if interval == "inf":
interval = "0.0"
# keep the radio buttons (random/customize) unchanged.
self._ui.custom_seed_sb.setValue(seed)
self.first_ef.setText(first)
self.interval_ef.setText(interval)
# FIXME: This `_AtomGroup' class and its methods may be rewritten based on the
# ``cms'' module. FIXME: Currently in the PT, we put on the component CTs, but
# it is better to put the full_system CT. In this case, stuff at here should be
# rewritten quite a bit.
# FIXME combine with AtomGroupModel
class _AtomGroup(object):
"""
Represents the data of the atom group table?
Of the Misc tab of the Advanced Options dialog
"""
FROZEN_NAME = "frozen"
# PREDEFINED_GROUP_NAME = [FROZEN_NAME,]
PREDEFINED_GROUP_NAME = [] # Ev:111176 Disable the "frozen" set for now
PREFIX_NAME = "i_ffio_grp_"
PREFIX_NAME_LEN = len(PREFIX_NAME)
def __init__(self, name):
# Atom groups are distinguished actually by two things: name and index.
# A `_AtomGroup' object contains all atom groups of the same name.
# N.B.: For position restraint atom groups, which have a predefined
# atom-group name, we don't need to save the name to `self.name`,
# and so we scavange (abuse) this attribute to save the
# force-constant value (float). This can be confusing!
self.name = name
# Key is index, value is the ASL that gives the selected atoms.
self.asl = {}
# Key is index, value is the number of selected atoms.
self.size = {}
# Key is index, value is a list of indices of selected atoms.
self.atom = {}
@staticmethod
def _default_group_selector(grp):
if (0 < len(grp.atom)):
return True
return False
@staticmethod
def _condense_atom_group(grp, group_selector):
ret_grp = collections.defaultdict(set)
for g in grp:
if group_selector(g):
for grp_index, atoms in g.atom.items():
if g.size[grp_index] > 0:
ret_grp[(g.name, grp_index)].update(atoms)
return ret_grp
@staticmethod
def get_asl(raw_grp):
"""
Returns an ASL string for a given group of atoms ('raw_grp').
:param raw_grp: Must be an iterable of 2-element tuples. The first
element of the tuple is the entry ID, the second is the atom ID
within the entry.
"""
# - Reorganize the raw groups into `grp'. `grp' is a dictionary with
# keys being the entry ID and values being a list of atom IDs (again,
# the IDs are entry-based IDs).
grp = {}
for a in raw_grp:
if (a[0] not in grp):
grp[a[0]] = []
grp[a[0]].append(int(a[1]))
asl = ""
for entry_num in grp:
grp[entry_num].sort()
g = grp[entry_num]
if (g is not []):
if (asl != ""):
asl += " or "
asl += ("(atom." + DES_SCRATCH_ENTRY_NUM_PROP + " %d and (atom."
+ DES_SCRATCH_ATOM_NUM_PROP) % int(entry_num)
seg_b = g[0]
seg_e = seg_b
for id_ in g[1:]:
if (id_ - 1 == seg_e):
seg_e = id_
else:
if (seg_b == seg_e):
asl += "%d, " % seg_b
else:
asl += "%d-%d, " % (seg_b, seg_e)
seg_b = id_
seg_e = id_
if (seg_b == seg_e):
asl += "%d))" % seg_b
else:
asl += "%d-%d))" % (seg_b, seg_e)
return asl
@staticmethod
def get_atom_grp(structs):
"""
Given a list of component CTs (`structs'), returns a list of
`'_AtomGroup'` objects
"""
# Gets the atom groups. Note that the predefined groups are before the
# user-defined groups.
atom_groups = []
grp_name = set()
for grp in _AtomGroup.PREDEFINED_GROUP_NAME:
grp_name.add(_AtomGroup.PREFIX_NAME + grp)
for ct in structs:
for name in list(ct.atom[1].property):
if (-1 < name.find(_AtomGroup.PREFIX_NAME)):
grp_name.add(name)
for name in grp_name:
atom_groups.append(_AtomGroup(name))
for grp in atom_groups:
sel_atom = {}
for ct in structs:
for atom in ct.atom:
entry_num = ct.atom[1].property.get(
DES_SCRATCH_ENTRY_NUM_PROP)
try:
value = atom.property[grp.name]
if (value > 0):
if (value not in sel_atom):
sel_atom[value] = set()
sel_atom[value].add((entry_num, int(atom)))
except KeyError:
# If one atom doesn't have this property, then all
# atoms in this CT shouldn't have it, either.
break
grp.atom = sel_atom
for val in sel_atom:
grp.size[val] = len(sel_atom[val])
grp.asl[val] = _AtomGroup.get_asl(sel_atom[val])
return atom_groups
@staticmethod
def del_all_atom_grp(structs):
for ct in structs:
for name in list(ct.atom[1].property):
if (-1 < name.find(_AtomGroup.PREFIX_NAME)):
ct.deletePropertyFromAllAtoms(name)
@staticmethod
def inject_atom_grp(structs, atom_grp):
"""
Updates the atom-level properties on the given model CTs
based on the given atom group list, and returns the list
of modified structs.
"""
# Removes the original atom groups.
_AtomGroup.del_all_atom_grp(structs)
if (atom_grp is not None):
# Constructs a dictionary from `structs'.
struc_ = {}
for ct in structs:
entry_num = ct.atom[1].property.get(DES_SCRATCH_ENTRY_NUM_PROP)
struc_[entry_num] = ct
# Now injects the new groups.
grp = _AtomGroup._condense_atom_group(
atom_grp, _AtomGroup._default_group_selector)
grp_name = []
for g, val in grp:
grp_name.append(g)
for gn in grp_name:
for ct in structs:
mm.mmct_atom_property_set(ct.handle, gn, mm.MMCT_ATOMS_ALL,
0)
for g, val in grp:
for entry_id, i_atom in grp[(
g,
val,
)]:
if (entry_id in struc_):
struc_[entry_id].atom[i_atom].property[g] = val
return structs
# FIXME: The following are for potision restraints management. It uses the
# `_AtomGroup' class to manage the selected atoms, so we put them under the
# class to avoid polluting the global namespace. It can be rewritten to use
# the `cms' module.
@staticmethod
def get_restr(structs):
"""
Return a list of AtomGroups for restraints in the given model CTs.
"""
restr = {}
for ct in structs:
ffh = ct.ffio.handle
n_restr = mm.mmffio_ff_get_num_restraints(ffh)
for i in range(1, n_restr + 1):
c1 = mm.mmffio_restraint_get_c1(ffh, i)
ai = mm.mmffio_restraint_get_ai(ffh, i)
if (c1 not in restr):
restr[c1] = set()
entry_num = ct.atom[1].property.get(DES_SCRATCH_ENTRY_NUM_PROP)
restr[c1].add((entry_num, ai))
atom_groups = []
for k in restr:
grp = _AtomGroup(k)
grp.atom[0] = restr[k]
grp.size[0] = len(grp.atom[0])
grp.asl[0] = _AtomGroup.get_asl(grp.atom[0])
atom_groups.append(grp)
return atom_groups
@staticmethod
def _restr_grp_selector(grp):
# For position-restraint atom groups, `grp.name` saves the force
# constant (float) of the corresponding restraint.
return (0 < grp.size[0] and 0.0 != grp.name)
@staticmethod
def resize_restr_block(ffh, size):
"""
Resizes the "ffio_restraints" block, i.e., changes the number of entries
of the block.
What will happen?
- If the new size is the same as the existing size, nothing changes.
- If the new size is smaller, the entries at the end of the block will
be deleted.
- If the new size is larger, new entries will be appended. These new
entries will have undetermined values.
:param ffh: The handle of the ffio block.
:param size: The wanted size of the restraint block.
"""
n_restr = mm.mmffio_ff_get_num_restraints(ffh)
if (size > n_restr):
size -= n_restr
mm.mmffio_add_restraints(ffh, size)
elif (size < n_restr):
for i in range(n_restr, size, -1):
mm.mmffio_delete_restraint(ffh, i)
@staticmethod
def set_restr(ffh, i, k, i_atom, x, y, z):
mm.mmffio_restraint_set_ai(ffh, i, i_atom)
mm.mmffio_restraint_set_funct(ffh, i, "harm")
mm.mmffio_restraint_set_c1(ffh, i, k)
mm.mmffio_restraint_set_c2(ffh, i, k)
mm.mmffio_restraint_set_c3(ffh, i, k)
mm.mmffio_restraint_set_t1(ffh, i, x)
mm.mmffio_restraint_set_t2(ffh, i, y)
mm.mmffio_restraint_set_t3(ffh, i, z)
@staticmethod
def set_restr_block(ct, restr):
ffh = ct.ffio.handle
size = len(restr)
i = 0
_AtomGroup.resize_restr_block(ffh, size)
for k, i_atom in restr:
atom = ct.atom[i_atom]
i += 1
_AtomGroup.set_restr(ffh, i, k, i_atom, atom.x, atom.y, atom.z)
@staticmethod
def inject_restr(structs, restr_grp):
"""
Sets up the restraints in the given components CTs based on the
state of the given restraint group.
"""
# Constructs a dictionary `struc_' from `structs'.
# Initializes a `restr_' dictionary, of which the key is the CT and the
# value is a list of tuples.
struc_ = {}
restr_ = {}
for ct in structs:
entry_num = ct.atom[1].property.get(DES_SCRATCH_ENTRY_NUM_PROP)
struc_[entry_num] = ct
restr_[ct] = []
# Constructs the `restr_' dictionary from `restr_grp'.
grp = _AtomGroup._condense_atom_group(restr_grp,
_AtomGroup._restr_grp_selector)
for gk in grp:
k = gk[0]
for entry_id, i_atom in grp[gk]:
if (entry_id in struc_):
restr_[struc_[entry_id]].append((k, i_atom))
# Constructs the ``ffio_restraints'' block.
for ct in structs:
_AtomGroup.set_restr_block(ct, restr_[ct])
[docs]class AtomGroupDelegate(QtWidgets.QItemDelegate):
"""
Class for allowing the data in the AtomGroupModel to be modified.
"""
[docs] def __init__(self):
QtWidgets.QItemDelegate.__init__(self)
[docs] def createEditor(self, parent, option, index):
if index.column() == 0:
editor = QtWidgets.QLineEdit(parent)
return editor
elif index.column() == 1:
editor = QtWidgets.QSpinBox(parent)
editor.setRange(1, 7) # FIXME why 1-7??
return editor
elif index.column() == 2:
editor = QtWidgets.QLineEdit(parent)
return editor
[docs] def setEditorData(self, editor, index):
if index.column() == 0:
# Group name
value = index.model().data(index, Qt.DisplayRole)
if not is_valid_groupname(value):
return
editor.setText(value)
elif index.column() == 1:
# Index
# FIXME may need to cast to QSpinBox
try:
value = int(index.model().data(index, Qt.DisplayRole))
except ValueError:
raise ValueError("AtomGrouModel.data() returned a non-int "
"value for column 1")
editor.setValue(value)
elif index.column() == 2:
# ASL
value = index.model().data(index, Qt.DisplayRole)
# FIXME may need to cast to QLineEdit
editor.setText(value)
[docs] def setModelData(self, editor, model, index):
if index.column() == 0:
# Group name
value = str(editor.text())
model.setData(index, value)
elif index.column() == 1:
editor.interpretText()
value = editor.value()
model.setData(index, value)
elif index.column() == 2:
value = str(editor.text())
model.setData(index, value)
[docs] def updateEditorGeometry(self, editor, option, index):
"""
"""
editor.setGeometry(option.rect)
[docs]class AtomGroupRow(_BaseAslRow):
[docs] def __init__(self, panel):
"""
:param guiapp Pass the guiapp instance.
"""
super(AtomGroupRow, self).__init__(panel)
self.index = 1
self.name = ""
[docs] def validate(self):
"""
Returns None if all values of this row are valid; error message
otherwise.
"""
if self.name == "":
return "No name specified"
# TODO: Validate self.index too?
return super(AtomGroupRow, self).validate()
[docs]class AtomGroupModel(QtCore.QAbstractTableModel):
"""
Class for storing the window table information.
"""
warning = QtCore.pyqtSignal(str)
[docs] def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.header = ["Group Name", "Index", "Atoms (ASL)", "# of atoms"]
self.rows = []
[docs] def setRows(self, rows):
self.beginResetModel()
self.rows = rows
self.endResetModel()
[docs] def clear(self):
self.setRows([])
[docs] def addOneRow(self, row):
row_idx = self.rowCount()
self.beginInsertRows(QtCore.QModelIndex(), row_idx, row_idx)
self.rows.append(row)
self.endInsertRows()
[docs] def removeOneRow(self, rownum):
self.beginRemoveRows(QtCore.QModelIndex(), rownum, rownum)
self.rows.pop(rownum)
self.endRemoveRows()
[docs] def rowCount(self, parent=QtCore.QModelIndex()): # noqa: M511
""" Returns number of rows """
return len(self.rows)
[docs] def columnCount(self, parent=QtCore.QModelIndex()): # noqa: M511
""" Returns number of columns """
return len(self.header)
[docs] def flags(self, index):
"""
Returns flags for the specified cell. Whether it is a checkbutton
or not.
"""
if not index.isValid():
return Qt.ItemIsSelectable
else:
if index.column() in [0, 1]:
# Group name or index column
row = self.rows[index.row()]
if row.name == _AtomGroup.FROZEN_NAME:
# The "frozen" row
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
else:
# Any "unnamed_X" row
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | \
Qt.ItemIsEditable
elif index.column() == 2:
# ASL
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | \
Qt.ItemIsEditable
else:
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
[docs] def data(self, index, role=Qt.DisplayRole):
"""
Given a cell index, returns the data that should be displayed in that
cell (text or check button state). Used by the view.
"""
if role == Qt.DisplayRole:
coli = index.column()
row = self.rows[index.row()]
if coli == 0:
return row.name
elif coli == 1:
return row.index
elif coli == 2:
return row.asl
elif coli == 3:
return len(row.atoms)
[docs] def setData(self, index, value, role=Qt.EditRole):
"""
Called by the view to modify the model when the user changes
the data in the table.
"""
if not index.isValid():
return False
coli = index.column()
row = self.rows[index.row()]
if role == Qt.EditRole:
if coli == 0:
row.name = value
return True
elif coli == 1:
row.index = value
return True
elif coli == 2:
try:
row.setAsl(value)
except ValueError:
self.warning.emit("Invalid ASL specified")
# Still change the value in the cell, as overwriting user's
# input will force them to re-type it in. Note that the row
# object is invalid if setAsl() raises ValueError.
return True
return False
[docs]class AtomGroupFrame(object):
"""
Frame for the "Atom group" box of the Misc tab of the Advanced Options
dialog.
"""
[docs] def __init__(self, master, key, ui):
self._master = master
self._ui = ui
self.num_unnamed_rows = 0
self.atomgroup_model = AtomGroupModel()
self.atomgroup_model.warning.connect(
lambda msg: QtWidgets.QMessageBox.warning(master._master, "Warning",
msg))
self.atomgroup_view = self._ui.atomgroup_view
self.atomgroup_view.setModel(self.atomgroup_model)
self.atomgroup_view.setItemDelegate(AtomGroupDelegate())
self.atomgroup_view.setSelectionBehavior(
QtWidgets.QAbstractItemView.SelectRows)
self.atomgroup_view.selectionModel().selectionChanged.connect(
self.updateAtomGroupButtons)
self.atomgroup_model.layoutChanged.connect(self.updateAtomGroupButtons)
self.atomgroup_model.modelReset.connect(self.updateAtomGroupButtons)
self.atomgroup_model.rowsInserted.connect(
lambda: self.updateAtomGroupButtons())
self.atomgroup_model.columnsInserted.connect(
lambda: self.updateAtomGroupButtons())
self.atomgroup_view.setColumnWidth(0, 100)
self.atomgroup_view.setColumnWidth(1, 80)
self.atomgroup_view.setColumnWidth(2, 150)
self.atomgroup_view.setColumnWidth(3, 110)
# FIXME column 0 EntryField, column 1 SpinBox, columnd 2 Entry
self.btn_select = self._ui.btn_select_2
self.btn_select.clicked.connect(self.selectAtomGroupAsl)
self.btn_add = self._ui.btn_add_2
self.btn_add.clicked.connect(self.addAtomGroupRow)
self.btn_delete = self._ui.btn_delete_2
self.btn_delete.clicked.connect(self.deleteAtomGroupRow)
self.btn_reset = self._ui.btn_reset_2
self.btn_reset.clicked.connect(self.resetAtomGroupTable)
self.resetAtomGroupTable()
self.updateAtomGroupButtons()
# __init__
[docs] def selectAtomGroupAsl(self):
irow = self.atomgroup_view.selectionModel().selectedRows()[0].row()
row = self.atomgroup_model.rows[irow]
title = "Desmond - Define '%s' atom-group" % row.name
new_asl = maestro.atom_selection_dialog(title, row.asl)
if new_asl:
row.setAsl(new_asl)
# Redraw the visible cells with new data:
self.atomgroup_view.viewport().update()
[docs] def addAtomGroupRow(self):
# Pass the GuiApp instance to AtomGroupRow.
# self._master is MdcMiscTab instance and self._master._master is
# GuiApp instance
row = AtomGroupRow(self._master._master)
row.name = "unnamed_" + str(self.num_unnamed_rows)
self.atomgroup_model.addOneRow(row)
self.num_unnamed_rows += 1
[docs] def deleteAtomGroupRow(self):
irow = self.atomgroup_view.selectionModel().selectedRows()[0].row()
self.atomgroup_model.removeOneRow(irow)
self.updateAtomGroupButtons()
def _sel_cmd(self, sel_rows):
for i in range(len(_AtomGroup.PREDEFINED_GROUP_NAME)):
try:
sel_rows.remove(i)
except:
pass
return sel_rows
[docs] def resetAtomGroupTable(self):
self.atomgroup_model.clear()
self.num_unnamed_rows = 0
for name in _AtomGroup.PREDEFINED_GROUP_NAME:
row = AtomGroupRow(self._master._master)
row.name = name
self.atomgroup_model.addOneRow(row)
self.updateAtomGroupButtons()
[docs] def getAtomGroups(self):
atom_groups = []
for row in self.atomgroup_model.rows:
try:
val = row.index
grp_name = _AtomGroup.PREFIX_NAME + row.name
grp = _AtomGroup(grp_name)
grp.atom[val] = row.atoms
grp.size[val] = len(grp.atom[val])
grp.asl[val] = row.asl
atom_groups.append(grp)
except ValueError:
# pass
raise # FIXME
return atom_groups
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
# TODO: ASL evaluation needs to be done at the time when the
# dialog is validated/closed instead of when the row's ASL
# field is edited.
for rowi, row in enumerate(self.atomgroup_model.rows, start=1):
err = row.validate()
if err:
return "Invalid data in the row %i of atom group table:\n%s" % (
rowi, err)
return None
[docs] def resetFromModel(self, model):
"""
Update the Atom group UI based on the given model CTs.
"""
atom_groups = _AtomGroup.get_atom_grp(model.comp_ct)
# Saves the original atom groups so that we can compare new group
# settings against them.
self.resetAtomGroupTable()
# Remarks the groups as not used.
i_name = _AtomGroup.PREFIX_NAME_LEN
for g in atom_groups:
g.is_used = False
i_row = 0
for grp in _AtomGroup.PREDEFINED_GROUP_NAME:
for g in atom_groups:
if (g.name[i_name:] == grp):
for val in g.atom:
row = self.atomgroup_model.rows[i_row]
row.atoms = g.atom[val]
row.index = val
row.asl = g.asl[val]
g.is_used = True
break
i_row += 1
for g in atom_groups:
if not g.is_used:
for val in g.atom:
row = AtomGroupRow(self._master._master)
row.atoms = g.atom[val]
row.name = g.name[i_name:]
row.index = val
row.asl = g.asl[val]
self.atomgroup_model.addOneRow(row)
self.num_unnamed_rows += 1
else:
g.is_used = False
self.num_unnamed_rows = 0
[docs] def updateKey(self, key):
# AtomGroupFrame
return 0
[docs] def updateFromKey(self, key):
pass
[docs]class MdcMiscTab(_MdcBase):
"""
Frame for the Misc tab of Mdc Advanced Options dialogs.
"""
[docs] def __init__(self, master, key, ui, include_randvel=True):
self._master = master
self._ui = ui
self.randvel_frame_object = None
mdc_widgets = []
self.ag_frame = AtomGroupFrame(self, key, ui)
self.cpt_info = self._ui.cpt_info_misc
self.cpt_info.setVisible(False)
self._ui.randvel_frame.hide()
mdc_widgets.append(self.ag_frame)
if include_randvel:
self.randvel_frame_object = RandVelFrame(self, key, ui, master)
self._ui.randvel_frame.show()
mdc_widgets.append(self.randvel_frame_object)
_MdcBase.__init__(self, mdc_widgets)
# __init__
[docs] def resetFromModel(self, model):
"""
- Expands and reset the atom group table if the given 'model' is a
'cms.Cms' object.
- Given a 'model'( which is a 'cms.Cms' object), resets the atom group
table.
"""
if model:
self.ag_frame.resetFromModel(model)
self._ui.atom_group_box.setEnabled(True)
self.cpt_info.setVisible(False)
else:
self._ui.atom_group_box.setEnabled(False)
self.cpt_info.setVisible(True)
return
[docs] def updateModel(self, model):
"""
Updates the given model to the data from the UI
"""
# Updates the atom groups and restraints. These are set into the.cms
# file (instead of the.cfg file).
atom_groups = self.ag_frame.getAtomGroups()
_AtomGroup.inject_atom_grp(model.comp_ct, atom_groups)
class _BaseAdvancedDialog(object):
"""
Subclass shared by MinAdvancedDialog and MdcAdvancedDialog.
"""
def __init__(self, master, key, pages, buttons, title, adv_tab, adv_ui):
self.adv_ui = adv_ui
self.adv_tab = adv_tab
self._master = master
self._tab_bg = None
self.tab_widgets = []
self.notebook = self.adv_ui.tab_notebook_widget
pagename_to_remove = []
for idx in range(self.notebook.count()):
a_widget = self.notebook.widget(idx)
remove_it = True
for pagename, the_widget in pages:
if str(self.notebook.tabText(idx)) == pagename:
self.tab_widgets.append(the_widget)
remove_it = False
break
if remove_it:
pagename_to_remove.append(self.notebook.tabText(idx))
# Have to remove tabs here b/c the index changes as you remove them
for page in pagename_to_remove:
for idx in range(self.notebook.count()):
if str(self.notebook.tabText(idx)) == page:
self.notebook.removeTab(idx)
break
if "Cancel" not in buttons:
raise ValueError("Cancel button is required")
self.cancel_command = buttons["Cancel"]
for name, command in future.utils.viewitems(buttons):
if name == "Apply":
self.adv_ui.apply_push_button.clicked.connect(command)
elif name == "OK":
self.adv_ui.ok_push_button.clicked.connect(command)
elif name == "Cancel":
self.adv_ui.cancel_push_button.clicked.connect(command)
elif name == "Help":
self.adv_ui.help_push_button.clicked.connect(command)
if title:
self.adv_tab.setWindowTitle(title)
self.adv_tab.rejected.connect(self.reject)
self.notebook.currentChanged.connect(self.currentTabChanged)
# FIXME self._tab_bg = self.notebook.tab( 0 )["bg"]
# __init__
def checkValidity(self):
"""
"""
for w in self.tab_widgets:
out = w.checkValidity()
if out:
return out
return None
def updateKey(self, key):
"""
"""
# _BaseAdvancedDialog
for w in self.tab_widgets:
w.updateKey(key)
def updateFromKey(self, key):
"""
"""
for w in self.tab_widgets:
w.updateFromKey(key)
def reject(self):
"""
Called when the user hits the Escape key.
"""
self.cancel_command()
def closeEvent(self, event):
"""
Called by QApplication when the user clicked on the "x" button
to close the advnaced dialog.
"""
self.cancel_command()
def currentTabChanged(self, page_index):
"""
"""
page = self.notebook.widget(page_index)
page.setEnabled(True)
def checkTab(self):
"""
"""
is_something_wrong = False
for i in range(self.notebook.count()):
page = self.notebook.widget(i)
out = page.checkValidity()
if out:
is_something_wrong = True
# FIXME if (self.notebook.getcurselection() != pn):
# FIXME self.notebook.tab( pn ).config( bg = "pink" )
return is_something_wrong
def setGuiState(self, state):
"""
Set the state of the advanced dialog
"""
for i in range(self.notebook.count()):
page = self.notebook.widget(i)
page.setEnabled(state)
def resetFromModel(self, model):
"""
Update the advanced dialog based on the state of the specified model CTs.
"""
for wgt in self.tab_widgets:
wgt.resetFromModel(model)
def updateModel(self, model):
"""
Update the key to the options that are in the advanced dialog.
"""
if model:
for wgt in self.tab_widgets:
wgt.updateModel(model)
def reset(self):
"""
Called when the "Reset" action is selected in the main window.
"""
self.restraints.reset()
[docs]class MinAdvancedDialog(_BaseAdvancedDialog):
"""
Advanced Minimization options dialog for Desmond panels
"""
[docs] def __init__(self, master, key, is_annealing, is_replica_exchange, buttons,
title):
# Base ui
self.adv_tab = QtWidgets.QDialog()
self.adv_ui = desmond_advanced_tab_ui.Ui_Dialog()
self.adv_ui.setupUi(self.adv_tab)
self.interaction = None
self.restraints = RestraintsTab(master, key, self.adv_ui)
self.output = OutputTab(master, key, self.adv_ui)
self.misc = MinMiscTab(master, key, self.adv_ui)
pages = [
("Restraints", self.restraints),
("Output", self.output),
("Misc", self.misc),
]
_BaseAdvancedDialog.__init__(self, master, key, pages, buttons, title,
self.adv_tab, self.adv_ui)
[docs]class MdcAdvancedDialog(_BaseAdvancedDialog):
"""
Advanced Options dialog for Mdc Desmond panels
"""
[docs] def __init__(self, master, key, is_annealing, is_replica_exchange, buttons,
title):
# Base ui
self.adv_tab = QtWidgets.QDialog(master)
self.adv_ui = desmond_advanced_tab_ui.Ui_Dialog()
self.adv_ui.setupUi(self.adv_tab)
self.integration = IntegrationTab(master, key, self.adv_ui)
if is_replica_exchange:
self.ensemble = None
else:
self.ensemble = EnsembleTab(master, key, is_annealing, self.adv_ui)
self.interaction = InteractionTab(master, key, self.adv_ui)
self.restraints = RestraintsTab(master, key, self.adv_ui)
self.output = OutputTab(master, key, self.adv_ui)
self.misc = MdcMiscTab(master, key, self.adv_ui)
pages = [
("Integration", self.integration),
("Ensemble", self.ensemble),
("Interaction", self.interaction),
("Restraints", self.restraints),
("Output", self.output),
("Misc", self.misc),
]
if is_replica_exchange:
pages.remove(("Ensemble", None))
_BaseAdvancedDialog.__init__(self, master, key, pages, buttons, title,
self.adv_tab, self.adv_ui)
[docs]class MinMiscTab(MdcMiscTab):
"""
Frame for the Misc Tab of Advanced Options dialog of Minimization GUI.
"""
[docs] def __init__(self, master, key, ui):
MdcMiscTab.__init__(self, master, key, ui, include_randvel=False)
[docs]class SimOptionsFrame(object):
"""
Responsible for drawing the simulation options widgets, including
simulation time, recording intervals and the number of frames.
"""
LE_WIDTH = 80 # LineEdit width
[docs] def __init__(self, master, key, app):
"""
See class docstring.
:param master: The root or master application
:type master: `af2.JobApp` object.
"""
self._master = master
self.app = app
self.setup()
[docs] def setup(self):
"""
Sets up the panel with SLineEdit widgets
"""
val = SNonNegativeRealValidator()
self.time_total_ef = SLineEdit("1.2",
validator=val,
width=self.LE_WIDTH,
always_valid=True)
self.time_total_ef.textEdited.connect(self.onSimTimeChanged)
self.app.ui.sim_gridLayout.addWidget(self.time_total_ef, 0, 0)
self.time_elapsed_ef = SLineEdit("0.0",
validator=val,
width=self.LE_WIDTH,
always_valid=True)
self.time_elapsed_ef.setEnabled(False) # make read-only
self.app.ui.sim_gridLayout.addWidget(self.time_elapsed_ef, 0, 2)
self.energy_inter_ef = SLineEdit("1.2",
validator=val,
width=self.LE_WIDTH,
always_valid=True)
self.app.ui.sim_gridLayout.addWidget(self.energy_inter_ef, 1, 2)
self.traj_inter_ef = SLineEdit("4.8",
validator=val,
width=self.LE_WIDTH,
always_valid=True)
self.traj_inter_ef.textEdited.connect(self.onTrajIntervalChanged)
self.app.ui.sim_gridLayout.addWidget(self.traj_inter_ef, 1, 0)
int_val = SNonNegativeIntValidator(bottom=1)
self.num_frames_ef = SLineEdit("250",
validator=int_val,
width=self.LE_WIDTH,
always_valid=True)
self.num_frames_ef.textEdited.connect(self.onNumOfFramesChanged)
self.app.ui.sim_gridLayout.addWidget(self.num_frames_ef, 2, 0)
self.app.ui.sim_gridLayout.addWidget(self.app.ui.sim_time_elapsed_label,
0, 1)
self.app.ui.sim_gridLayout.addWidget(self.app.ui.energy_label, 1, 1,
Qt.AlignRight)
self.output_label = self.app.ui.sim_time_elapsed_label
[docs] def onTrajIntervalChanged(self):
"""
Called when trajectory interval changes.
"""
if self.traj_inter_ef.hasAcceptableInput():
sim_time = float(self.time_total_ef.text()) * 1000 # ns to ps
traj_interval = float(self.traj_inter_ef.text())
if traj_interval != 0.0:
num_frames = int(old_div(sim_time, traj_interval))
self.num_frames_ef.setText(str(num_frames))
[docs] def onNumOfFramesChanged(self, num_frames_str):
"""
Called when the number of frames changes.
:param num_frames_str: Value of the number of frames field.
:type num_frames_str: str
"""
if not num_frames_str:
return
if int(num_frames_str) != 0:
sim_time = float(self.time_total_ef.text()) * 1000 # ns
traj_interval = round(old_div(sim_time, int(num_frames_str)), 1)
self.traj_inter_ef.setText(str(traj_interval))
[docs] def setEnabledExceptSimulationTime(self, enable):
for widget in [
self.time_elapsed_ef, self.output_label, self.energy_inter_ef,
self.traj_inter_ef, self.num_frames_ef
]:
widget.setEnabled(enable)
[docs] def onSimTimeChanged(self):
"""
Called when simulation time changes.
"""
# Total time should be larger than the elapsed time.
if self.time_total_ef.hasAcceptableInput():
t = float(self.time_total_ef.text())
t1 = float(self.time_elapsed_ef.text())
if (t < t1):
self.time_total_ef.setToolTip(
"Total time should be larger than the elapsed time.")
else:
self.time_total_ef.setToolTip("")
if t < 4.8: # in ns
# always set traj interval to 4.8 as default.
self.traj_inter_ef.setText("4.8")
num_frames = int(t * 1000 / 4.8)
self.num_frames_ef.setText(str(num_frames))
else:
# t(ns)/1000 = t(ps)
self.traj_inter_ef.setText(str(t))
self.num_frames_ef.setText("1000")
[docs] def setGuiState(self, state):
self.energy_inter_ef.setGuiState(state)
self.traj_inter_ef.setGuiState(state)
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
for_validation = [
self.time_total_ef, self.time_elapsed_ef, self.energy_inter_ef,
self.traj_inter_ef
]
for item in for_validation:
if not item.hasAcceptableInput():
return "Invalid value: '%s'" % item.text()
t = float(self.time_total_ef.text())
t1 = float(self.time_elapsed_ef.text())
if (t < t1):
return "Elapsed time must be less than total time"
return None
[docs] def updateKey(self, key):
"""
Update the key based on the GUI settings
"""
# SimOptionsFrame
key.time.val = float(self.time_total_ef.text()) * 1000.0
if self.time_elapsed_ef.isEnabled():
key.elapsed_time.val = float(self.time_elapsed_ef.text()) * 1000
key.eneseq.interval.val = float(self.energy_inter_ef.text())
key.trajectory.interval.val = float(self.traj_inter_ef.text())
[docs] def updateFromKey(self, key):
"""
Updates widgets based on key.
"""
self.time_total_ef.setText(str(key.time.val * 0.001))
self.time_elapsed_ef.setText(str(key.elapsed_time.val * 0.001))
self.energy_inter_ef.setText(str(key.eneseq.interval.val))
self.traj_inter_ef.setText(str(key.trajectory.interval.val))
# The number of frames entry doesn't update automatically when the other
# entries are changed programatically - force an update
self.onTrajIntervalChanged()
[docs]class EnsembleClassFrame(object):
"""
Frame that includes the Ensemble class pull-down menu and the entry fields
correspondint to it.
"""
TOOLTIP_TEXT = {
u"NVE": "Constant number of atoms, constant volume, constant energy",
u"NVT": "Constant number of atoms, constant volume, constant temperature",
u"NPT": "Constant number of atoms, constant pressure, constant temperature",
u"NP\u03B3T": "Constant number of atoms, constant surface tension, constant temperature",
u"NPAT": "Constant number of atoms, constant area, constant temperature",
}
[docs] def __init__(self, master, key, ens_callback, temp_callback, is_annealing,
is_replica_exchange, app):
"""
See class docstring.
"""
self.app = app
self._master = master
self._ens_callback = ens_callback
self._temp_callback = temp_callback
self.is_annealing = is_annealing
self._default_key = copy.deepcopy(key)
self.ens_class_menu = self.app.ui.ensemble_class_combo_box
self.temp_ef = self.app.ui.temperature_line_edit
self.temp_label = self.app.ui.label_2
if not self.is_annealing:
# FIXME the original simply did not grid this widget if False
self.temp_ef.setValidator(
SNonNegativeRealValidator(self.app,
top=100000000.0,
decimals=100))
self.temp_ef.textEdited.connect(self.changeTemp)
if is_annealing or is_replica_exchange:
self.temp_label.setVisible(False)
self.temp_ef.setVisible(False)
self.pressure_ef = self.app.ui.pressure_line_edit
self.pressure_ef.setValidator(
SRealValidator(self.app,
bottom=-100000000.0,
top=100000000.0,
decimals=100))
self.tension_label = self.app.ui.surface_tension_label
self.tension_ef = self.app.ui.surface_tension_line_edit
self.tension_ef.setValidator(
SNonNegativeRealValidator(self.app, top=100000000.0, decimals=100))
self.ens_class_menu.currentIndexChanged.connect(self.ensClassChanged)
# __init__
[docs] def ensClassChanged(self, ens_class, should_callback=True):
"""
"""
if type(ens_class) == type(1):
ens_class = self.ens_class_menu.currentIndex()
ens_class = self.ens_class_menu.currentText()
if self.is_annealing:
if ens_class == "NVT":
self.pressure_ef.setEnabled(False)
else:
self.pressure_ef.setEnabled(True)
else:
if ens_class == "NVE":
self.temp_ef.setEnabled(False)
self.pressure_ef.setEnabled(False)
elif ens_class == "NVT":
self.temp_ef.setEnabled("," not in self.temp_ef.text())
self.pressure_ef.setEnabled(False)
else:
self.temp_ef.setEnabled("," not in self.temp_ef.text())
self.pressure_ef.setEnabled(True)
if ens_class == u"NP\u03B3T":
self.tension_label.setEnabled(True)
self.tension_ef.setEnabled(True)
else:
self.tension_label.setEnabled(False)
self.tension_ef.setEnabled(False)
# Set the tooltip that is appropriate for this ensemble class:
for key, value in future.utils.viewitems(
EnsembleClassFrame.TOOLTIP_TEXT):
if key == ens_class:
self.ens_class_menu.setToolTip(value)
if should_callback:
self._ens_callback(ens_class)
[docs] def changeTemp(self):
if not self.temp_ef.isReadOnly():
temp = self.temp_ef.text()
if temp:
# Avoid changing the temp SEA atom to integer type (MATSCI-3560)
temp = str(float(temp))
self._temp_callback(temp)
[docs] def updateEnsClassFromKey(self, key):
ens_class = key.ensemble.class_.val
if ens_class == u"NPgT":
ens_class = u"NP\u03B3T"
if self.is_annealing and ens_class == "NVT":
pass
# NOTE: Ev:113158 Simulated annealing does not support NVE ensemble
# class.
# ens_class = u"NVE"
self.ens_class_menu.setToolTip(
EnsembleClassFrame.TOOLTIP_TEXT[ens_class])
index = self.ens_class_menu.findText(ens_class)
if index == -1:
raise ValueError("Key not in ensemble class menu: %s" % ens_class)
self.ens_class_menu.setCurrentIndex(index)
self.ensClassChanged(ens_class, should_callback=False)
[docs] def updateTempFromKey(self, key):
t_tmp = [t for t, g in key.temperature]
is_uniform = True
t0 = t_tmp[0]
for t in t_tmp:
if not sea.is_equal(t.val, t0.val):
is_uniform = False
break
if is_uniform:
self.temp_ef.setText(str(t0))
self.temp_ef.setValidator(
SNonNegativeRealValidator(self.app,
top=100000000.0,
decimals=100))
self.temp_ef.setEnabled(True)
else:
self.temp_ef.setEnabled(False)
self.temp_ef.setValidator(None) # Remove the validator
s = str(t0)
for t in t_tmp[1:]:
s += ", " + str(t)
self.temp_ef.setText(s)
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
for_validation = []
if not self.is_annealing:
for_validation.append(self.temp_ef)
ens_class = self.ens_class_menu.currentText()
if ens_class == "NVT":
pass
elif ens_class == "NPT":
for_validation.extend([
self.pressure_ef,
])
elif ens_class == "NPAT":
for_validation.extend([
self.pressure_ef,
])
elif ens_class == u"NP\u03B3T":
for_validation.extend([
self.pressure_ef,
self.tension_ef,
])
for item in for_validation:
if not item.hasAcceptableInput():
return "Invalid value: '%s'" % item.text()
return None
[docs] def updateKey(self, key):
# EnsembleClassFrame
ens_class = self.ens_class_menu.currentText()
if ens_class == u"NP\u03B3T":
ens_class = "NPgT"
key.ensemble.class_.val = ens_class
if ens_class == "NVE":
key.ensemble.method.val = "NH"
elif key.ensemble.method.val == "MTK" and ens_class == "NVT":
key.ensemble.method.val = "NH"
elif key.ensemble.method.val == "NH" and ens_class in [
"NPT",
"NPAT",
"NPgT",
]:
key.ensemble.method.val = "MTK"
if self.pressure_ef.isEnabled():
pressure_val = str(self.pressure_ef.text())
else:
pressure_val = self._default_key.pressure[0].val
key.pressure[0].val = pressure_val
if self.tension_ef.isEnabled():
surface_tension_val = str(self.tension_ef.text())
else:
surface_tension_val = self._default_key.surface_tension.val
key.surface_tension.val = surface_tension_val
if not self.is_annealing:
if self.temp_ef.isEnabled():
temp = self.temp_ef.text()
if temp:
# Avoid changing the temp SEA atom to integer type
# (MATSCI-3560)
temp = str(float(temp))
for e, g in key.temperature:
e.val = temp
else:
key.temperature = copy.deepcopy(self._default_key.temperature)
[docs] def updateFromKey(self, key):
self.pressure_ef.setText(str(key.pressure[0].val))
self.tension_ef.setText(str(key.surface_tension.val))
if 'annealing' in key and not key.annealing.val:
self.updateTempFromKey(key)
self.updateEnsClassFromKey(key)
[docs] def model_changed_callback(self, model):
if model is not None and model.get_membrane_cts():
# If membrane is present, set default ENS to NPgT
ens_class = u"NP\u03B3T"
index = self.ens_class_menu.findText(ens_class)
self.ens_class_menu.setCurrentIndex(index)
[docs]class RelaxFrame(object):
"""
Frame that includes the "Relax model..." check box and the relaxation
protocol entry field.
"""
[docs] def __init__(self, master, key, app):
"""
See class docstring
"""
self.app = app
self._master = master
self._pfile = None
self._bad_pfile = False
self.relax_box = self.app.ui.relax_model_check_box
self.relax_box.toggled.connect(self.toggleRelaxModel)
self.prot_file = cwidget.FileEntry(
self.app.ui.simulation_group_box,
"",
"Desmond - Specify Relaxation Protocol File",
';;'.join([
'Protocol files (*.msj)',
'All files (*)',
]),
command=self.changeProtFile,
)
self.app.ui.relaxation_horizontal_layout.addWidget(self.prot_file)
self.app.ui.relaxation_horizontal_layout.addStretch()
[docs] def model_changed_callback(self, model):
if model is None:
self.relax_box.setChecked(False)
self.toggleRelaxModel()
is_membrane_system = bool(model and model.get_membrane_cts())
if is_membrane_system:
text = "Relax membrane model system before simulation"
else:
text = "Relax model system before simulation"
self.relax_box.setText(text)
self.prot_file.setEnabled(not is_membrane_system)
self.app.ui.relaxation_label.setEnabled(not is_membrane_system)
[docs] def shouldRelax(self):
return self.relax_box.isChecked()
[docs] def getProtFilename(self):
"""
Returns the name of the user-specified protocol file or None if user
did not specifies any file.
"""
if self.relax_box.isChecked():
return self._pfile
else:
return None
[docs] def toggleRelaxModel(self, ignored=None):
if self.relax_box.isChecked():
self.app.ui.relaxation_label.setEnabled(True)
self.prot_file.setEnabled(True)
else:
self.app.ui.relaxation_label.setEnabled(False)
self.prot_file.setEnabled(False)
[docs] def changeProtFile(self, fname):
if (fname != ""):
if (os.path.isfile(fname)):
self._bad_pfile = False
else:
self._bad_pfile = True
self._pfile = fname
else:
self._pfile = None
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
if self.relax_box.isChecked() and self._bad_pfile:
return "Prot file does not exist"
# If membrane is present, verify that solvent is also present:
model = self.app._model if hasattr(self.app, '_model') else None
if model and model.get_membrane_cts() and not model.get_solvent_cts():
return "Input structure must contain solvent CT to run membrane relaxation"
return None
[docs] def updateKey(self, key):
"""
Update the key based on the GUI settings
"""
# RelaxFrame
[docs] def updateFromKey(self, key):
# FIXME
pass
[docs]class AdvOptionsFrame(QtCore.QObject):
"""
Frame for the "Advanced Options" button.
"""
MDC_DEFAULT_HEIGHT = 350
MDC_DEFAULT_WIDTH = 480
[docs] def __init__(self,
master,
key,
win,
mdc_callback,
adv_dialog_class,
is_annealing,
is_replica_exchange,
app,
dialog_master=None):
"""
See class docstring.
:type dialog_master: QWidget
:param dialog_master: The widget that will be the master widget of the
Advanced Options dialog. If not provided, the app argument will be
used.
"""
QtCore.QObject.__init__(self)
self.app = app
self._master = master
self._win = win
self._mdc_callback = mdc_callback
# Class which will be used to create the advanced options window
self._adv_dialog_class = adv_dialog_class
self.saved_ag_model = None
# self._adv_dialog_class will be set to either MinAdvancedDialog or
# MdcAdvancedDialog
self.advopts_button = self.app.ui.advanced_options_button
self.advopts_button.clicked.connect(self.openAdvancedDialog)
# Reference to the adnvaced options dialog:
# Instance of _BaseAdvancedDialog
if not dialog_master:
dialog_master = self.app
self.dialog = self._adv_dialog_class(
dialog_master,
key,
is_annealing=is_annealing,
title="%s - Advanced Options" % self._win.title,
buttons={
"OK": self.okPressed,
"Apply": self.applySettings,
"Cancel": self.cancelPressed,
"Help": self.helpPressed,
},
is_replica_exchange=is_replica_exchange)
[docs] def updateDialogKeys(self):
"""
Update the keys of the dialog from the options.
"""
self._win._update_key()
self.dialog.updateFromKey(self._win._key)
[docs] def openAdvancedDialog(self):
self.updateDialogKeys()
self.dialog.adv_tab.setWindowModality(Qt.WindowModal)
self.dialog.adv_tab.show()
[docs] def model_changed_callback(self, model):
self.dialog.resetFromModel(self._win._model)
[docs] def closeDialog(self):
"""
Close the advanced options dialog.
"""
self.dialog.adv_tab.close()
[docs] def hideDialog(self):
"""
"""
self.dialog.adv_tab.hide()
[docs] def okPressed(self):
"""
Called when the "OK" button of the advanced dialog is pressed.
Closes the dialog only if "Apply" operation was successful.
"""
if self.applySettings() == 0:
# There were no errors
self.hideDialog()
[docs] def applySettings(self):
"""
Called when the "Apply" button of the advanced dialog is pressed.
Returns 0 on success, 1 if any of the widgets had invalid values.
"""
out = self.checkValidity()
if out:
error_dialog(self, out + "\n\nPlease correct the settings.")
return 1
# Update the key from the dialog's options:
self.dialog.updateKey(self._win._key)
# Update the model to the data from the UI:
self.dialog.updateModel(self._win._model)
self.saved_ag_model = self.dialog.misc.ag_frame.atomgroup_model
# Tell other classes that the model was changed:
self._mdc_callback()
#
# FIXME do we need to update other widgets???
return 0
[docs] def cancelPressed(self):
"""
Called when the "Cancel" button of the advanced dialog is pressed.
"""
self.hideDialog()
# Update the dialog from the key (revert user's changes):
self.dialog.updateFromKey(self._win._key)
[docs] def helpPressed(self):
maestro.command("helptopic DESMOND_ADVANCED_OPTIONS_DB")
maestro.command("showpanel help")
[docs] def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
return self.dialog.checkValidity()
[docs] def updateKey(self, key):
# AdvOptionsFrame
# Need to update key for the RandVelFrame to have different random seed
# to be written to .cfg file every time GuiApp._write_cfg() is called.
self.dialog.updateKey(key)
[docs] def updateFromKey(self, key):
self.dialog.updateFromKey(key)
class _BaseGroup(object):
"""
Inherited by MdGroup and MinimizeGroup, so therefore by all Simulation
frames
"""
def __init__(self, master, key, win, group_name="Simulation"):
self.app = master
self._master = master
self._win = win
# Subclass is responsible for setting correctly. Used by `checkValidity'
# method (see below).
self._widgets = []
def model_changed_callback(self, model):
for w in self._widgets:
if (hasattr(w, "model_changed_callback")):
w.model_changed_callback(model)
def adv_callback(self):
"""
Called when the new Advanced Options are applied.
"""
self._win.updateFromKey(self._win._key)
def checkValidity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
for w in self._widgets:
out = w.checkValidity()
if out:
return out
return None
def updateKey(self, key):
"""
Updates the key based on the GUI settings.
"""
# _BaseGroup
for w in self._widgets:
w.updateKey(key)
def updateFromKey(self, key):
"""
Set this frame to the values in key.
"""
for w in self._widgets:
w.updateFromKey(key)
[docs]class MdGroup(_BaseGroup):
"""
Molecular Dynamics group
"""
[docs] def __init__(self,
master,
key,
win,
is_annealing=False,
is_replica_exchange=False,
dialog_master=None):
"""
:type dialog_master: QWidget
:param dialog_master: The widget that will be the master widget of the
Advanced Options dialog. If not provided, the master argument will be
used.
"""
_BaseGroup.__init__(self, master, key, win)
self.app = master
self.opts = SimOptionsFrame(self, key, self.app)
self.ens = EnsembleClassFrame(self, key, self._change_ens,
self._change_temp, is_annealing,
is_replica_exchange, self.app)
self.relx = RelaxFrame(self, key, self.app)
self.adv_frame = AdvOptionsFrame(self.app.ui.simulation_group_box,
key,
win,
self.adv_callback,
MdcAdvancedDialog,
is_annealing,
is_replica_exchange,
self.app,
dialog_master=dialog_master)
self._widgets = [
self.opts,
self.ens,
self.relx,
self.adv_frame,
]
# __init__
[docs] def setEnabledExceptSimulationTime(self, enable):
"""
Ev:111016
Enable/disable this group. Does NOT disable the simulation time entry
field.
"""
for widget in self._widgets:
if widget == self.opts:
widget.setEnabledExceptSimulationTime(enable)
else:
widget.setEnabled(enable)
def _change_ens(self, ens_class):
if not hasattr(self, "adv_frame"):
return
if not self.adv_frame.dialog.ensemble: # REMD GUI no ensemble ,
return
if self.adv_frame.dialog:
self.adv_frame.dialog.ensemble.resetEnsClass(ens_class)
def _change_temp(self, temp):
if self.adv_frame.dialog:
self.adv_frame.dialog.ensemble.resetThermoTemp(temp)
[docs]class DesmondBasicConfigDialog(config_dialog.ConfigDialog):
"""
This is identical to Appframework.ConfigDialog except adds platform check
for localhost jobs
"""
[docs] def __init__(self, *args, **kwargs):
"""
Adds a validate_platforms property that indicates whether the platform
should be validated if it is localhost
"""
config_dialog.ConfigDialog.__init__(self, *args, **kwargs)
self.validate_platforms = True
[docs] def validate(self):
"""
Performs localhost validate if requested and then calls the parent class
validation method
:rtype: bool
:return: Whether the dialog settings validate properly or not
"""
if self.validate_platforms:
host = self.currentHost()
# Desmond does not run on all platforms
if not platforms.validate_host(host):
self.warning(platforms.PLATFORM_WARNING)
return False
return config_dialog.ConfigDialog.validate(self)
Super = af2.JobApp # Use for job panels
[docs]class GuiApp(Super):
"""
Base class of several Desmond panel GUI's (see its subclasses below).
"""
[docs] def __init__(
self,
key,
validator, # e.g. config.DEFAULT_SETTING.VALIDATE_MD,
tag_spec, # e.g. config.TAG_SPECS.md,
sim_class, # Can be gui.MdGroup, SaGroup, gui.FEP, MetadynamicsGroup, MinimizeGroup,
is_replica_exchange=False,
**kwargs):
# Housekeeping data
# Database of backend configuration, updated by the GUI.
self._key = config.canonicalize(key)
# For resetting the panel to defaults
self._default_key = copy.deepcopy(self._key)
# Used to check a given `key'
self._validator = validator
self._tag_spec = tag_spec
self._sim_class = sim_class
self.is_replica_exchange = is_replica_exchange
Super.__init__(self, **kwargs)
[docs] def setup(self):
Super.setup(self)
self._widgets = []
# Are we showing the setup in a checkpoint file?
self._is_cpt = False
self._model = None
# All widgets on the main panel.
# Widgets that wish to handle the model-system-changed event must
# define a `model_changed_callback' method. For the
# interface of this method, see `model_changed_callback' below.
# Set up the Model system input widgets at the top of the panel
self.model_sys = InputGroup(self, self.model_changed_callback)
layout = self.main_layout
layout.insertWidget(0, self.model_sys.group_box)
# Create an instance of gui.MdGroup, SaGroup, gui.FEP,
# MetadynamicsGroup, or MinimizeGroup class:
self.sim = self._sim_class(self, self._key, self)
[docs] def layOut(self):
"""
See `af2.JobApp` documentation for method documentation.
"""
Super.layOut(self)
# Add a Desmond logo (equivalent to the AF1's show_desres_icon):
desres_layout = af1.make_desres_layout()
self.main_layout.addLayout(desres_layout)
[docs] def getConfigDialog(self):
return DesmondBasicConfigDialog(self,
incorporation=True,
host=True,
cpus=True,
default_disp=af2.DISP_APPEND)
[docs] def enableSimulationGroupBox(self, state):
"""
Set the simulation group box enabled state
:type state: bool
:param state: True if the groupbox should be enabled, False if not
"""
self.ui.simulation_group_box.setEnabled(state)
[docs] def model_changed_callback(self, model):
"""
The GUI is designed to have some intelligence that can adapt its own
look based on the imported model system. So if the model system is
changed, we need to inform some other parts of the GUI.
The event that the model system is changed will be initially triggered
from the `InputGroup` (see this class above), then it will go to here
(i.e., `GuiApp`), and from here the signal will be sent forth to all
interested parts of the GUI.
:param model: A `cms.Cms` object, containing all information about the
model system. If it is none, then the model is a checkpoint file.
"""
self._model = model
if (model is None):
try:
self.sim.opts.setEnabledExceptSimulationTime(False)
except AttributeError:
self.enableSimulationGroupBox(False)
self.sim.opts.setEnabled(False)
# FIXME: It's not possible to disable the "Read" action in AF2
# if self._buttons.get('read'):
# self._buttons["read"].setEnabled(False)
self._is_cpt = True
self._key = self.model_sys._key
if (isinstance(self._key.cpu, sea.Atom)):
# Changes the value of cpu to a list.
import schrodinger.application.desmond.autopartition as autopartition
x, y, z = autopartition.auto_partition([
80,
80,
80,
], self._key.cpu.val)
self._key["cpu"] = [int(x), int(y), int(z)]
self.updateFromKey(self._key)
else:
try:
self.sim.opts.setEnabledExceptSimulationTime(True)
except AttributeError:
self.enableSimulationGroupBox(True)
# FIXME: It's not possible to disable the "Read" action in AF2
# if self._buttons.get('read'):
# self._buttons["read"].setEnabled(True)
self._is_cpt = False
for w in self.sim._widgets:
if (hasattr(w, "model_changed_callback")):
w.model_changed_callback(model)
if hasattr(self.sim, "adv_frame") and self.sim.adv_frame.dialog:
self.sim.adv_frame.updateDialogKeys()
[docs] def updateFromKey(self, key):
"""
Updates the look of the GUI with `key`
"""
for w in self.sim._widgets:
w.updateFromKey(key)
def _check_validity(self):
"""
Checks if all widgets have valid values. If a widget with invalid
value was encountered, then returns string that describes the problem.
Returns None if there are no issues.
"""
for w in self.sim._widgets:
out = w.checkValidity()
if out:
return out
return None
def _update_key(self):
"""
- Update the `self._key` based on the GUI settings.
- Will call `_update_key` methods of each element in the
`self._widgets` list.
- Returns error string if updating failed, or None if everything is OK.
"""
out = self._check_validity()
if out:
return out
for w in self.sim._widgets:
w.updateKey(self._key)
return None
def _submit_job(self,
jobname,
incorp,
host,
cpu,
sys_fname,
cfg_fname=None,
really_start=True):
"""
Submit a $SCHRODINGER/desmond job.
:param cpu: should be the total number of CPUs. For REMD jobs, the
total number of CPUs = number of CPUs for each replica times number
of replicas. REMD jobs are started via this method only when
restarting from a checkpoint file.
"""
# Constructs the command line for job launching. Use forward slashes
# on all platforms
cmd_line = [
'${SCHRODINGER}/desmond',
"-PROJ",
maestro.project_table_get().fullname,
"-DISP",
incorp,
"-VIEWNAME",
self.viewname,
"-JOBNAME",
jobname,
"-HOST",
host,
"-PROCS",
str(cpu),
]
if (self._is_cpt):
# Restarting from a check point file
if (self.gpu):
cmd_line += ["-gpu"]
input_cms_fname = get_in_cms_from_cpt(sys_fname)
in_cms = os.path.basename(input_cms_fname)
# PANEL-11376 - Copy CPT to job dir and use the copy
job_cpt = jobname + '-in.cpt'
try:
shutil.copyfile(input_cms_fname, in_cms)
shutil.copyfile(sys_fname, job_cpt)
except shutil.IOError as e:
self.error(e)
return
cmd_line += ["-restore", job_cpt]
cmd_line += ["-in", in_cms]
if self.is_replica_exchange:
cmd_line += [
"-cfg",
"remd.last_time=" + str(self._key.time.val),
]
else:
cmd_line += [
"-cfg",
"mdsim.last_time=" + str(self._key.time.val),
]
else:
cmd_line = [
"-in",
sys_fname,
"-c",
cfg_fname,
]
# Appends the command string into the.cfg file. Some customers find
# this is useful so they can copy and paste
# the command and launch the job from a console.
cmd_str = subprocess.list2cmdline(cmd_line)
util.append_comment(cfg_fname, [
"Job launching command:",
cmd_str,
])
if really_start:
try:
job = jobcontrol.launch_job(cmd_line)
except RuntimeError as e:
self.warning("Job submission failed.\n\nERROR: %s" % str(e))
return
return job
else:
self.writeJobCmd(cmd_line)
return None
def _submit_multisim_job(
self,
jobname,
incorp,
whost,
chost,
cpus,
maxjob,
sys_fname,
cfg_fname,
msj_fname,
out_fname,
other_options=[], # noqa: M511
really_start=True):
"""
Submit a $SCHRODINGER/utilities/multisim job.
:param whost: The master host, and optionally the processor count,
delimited with a colon. The master job normally requires only
1 processor to run. In umbrella mode (e.g. Replica Exchange),
in order to run on multiple hosts, the proc count should be
explicitly specified.
Each subjob itself can use multiple processors, see cpus option.
:type whost: str
:param cpus: The number of processors for each subjob to use (NOT the
total processor count). Will be deprecated in the future. For
Replica Exchange, this should be set to 1.
:type cpus: int
"""
# Constructs the command line for job launching, use forward slashes
# on all platforms
cmd_line = [
# Make sure forward slashes are used on all platforms:
'${SCHRODINGER}/utilities/multisim',
"-VIEWNAME",
self.viewname,
"-JOBNAME",
jobname,
"-HOST",
whost,
"-maxjob",
str(maxjob),
"-cpu",
"%d" % (cpus),
"-m",
msj_fname,
"-c",
cfg_fname,
"-description",
str(self.title),
sys_fname,
] + other_options
if maestro:
proj_name = maestro.project_table_get().fullname
cmd_line.extend(["-PROJ", proj_name, "-DISP", incorp])
if out_fname:
cmd_line.extend([
"-o",
out_fname,
])
if chost:
cmd_line.extend([
"-SUBHOST",
chost,
])
if sys_fname:
model = cms.Cms(file=sys_fname)
if util.use_custom_oplsdir(model.fsys_ct):
opls_dir = forcefield.get_custom_opls_dir()
# verify that the opls_dir is valid and allow for using default
opls_dir_result = self.validateOPLSDir(opls_dir)
if opls_dir_result == OPLSDirResult.VALID:
cmd_line += ["-OPLSDIR", opls_dir]
else: # ABORT
return
cmd_line = license.add_md_lic(cmd_line)
# Appends the command string into the.msj file. Some customers find
# this is useful so they can copy and paste the
# command and launch the job from a console.
cmd_str = subprocess.list2cmdline(cmd_line)
util.append_comment(msj_fname, [
"Job launching command:",
cmd_str,
])
if really_start:
try:
job = jobcontrol.launch_job(cmd_line)
except RuntimeError as e:
self.warning("Job submission failed.\n\nERROR: %s" % str(e))
return None
return job
else:
# Writes the job command to jobname.sh
self.writeJobCmd(cmd_line)
return None
def _set_detail(self, n_cpu):
# If a model is imported, we get the box size from the model; othewise,
# we get an arbitrary size.
size = cms.get_boxsize(self._model.box) if (self._model) \
else [80.0, 80.0, 80.0, ]
from schrodinger.application.desmond.autopartition import auto_partition
return auto_partition(size, n_cpu)
@af2.validator()
def verifySettings(self):
"""
Called by AppFramework to verify that all UI elements have valid
states. Returns True if so. Returns False and the error message if
any issues are found.
"""
# Skip validation if started from KNIME
if self.in_knime:
return True
err = self._update_key()
if err:
return (False, "Error with parameter value:\n%s" % err)
if not self._is_cpt and self._model is None:
return (False, "The model system file has not been given to run"
" a simulation.")
return True
[docs] @af2.appmethods.start()
def startCommand(self, really_start=True):
"""
Writes the job input files and assembles the command to execute.
:type really_start: bol
:param really_start: Whether to start the job after writing the input files.
"""
cd_params = self.configDialogSettings()
n_replica = 1
if self.is_replica_exchange:
if isinstance(self._key.replica, sea.List):
n_replica = len(self._key.replica)
else:
# REST Job
n_replica = self._key.replica.temperature.n_replica.val
total_cpus = cd_params['cpus']
cpus = old_div(total_cpus, n_replica)
cpus = 1 if (cpus == 0) else cpus
self._key["cpu"] = cpus
jobname = self.jobname()
incorp = cd_params['disp']
host = cd_params['host']
maxjob = cd_params.get('njobs')
if not maxjob:
maxjob = 1
if self._is_cpt:
# Start from a check point file
return self._submit_job(jobname,
incorp,
host,
cpus * n_replica,
self.model_sys.cpt_fname,
"",
really_start=really_start)
try:
gpus = cd_params.get('gpus')
if gpus:
os.environ['SCHRODINGER_CUDA_VISIBLE_DEVICES'] = \
','.join([str(g) for g in gpus])
except:
gpus = None
use_gpu = bool(gpus)
if not use_gpu:
msg = ("WARNING: CPU Desmond is being deprecated! Please migrate "
"your CPU Desmond applications to GPU. Please contact "
"your account manager to discuss a transition to Desmond "
"GPU licenses.")
mbox = messagebox.MessageBox(
title="Desmond CPU Deprecation Warning",
text=msg,
save_response_key="desmond_cpu_warning")
mbox.exec()
print(msg)
self.status_bar.setStatus("WARNING: CPU Desmond is deprecated!")
if self._key.ensemble.method.val == DPD:
if not use_gpu:
self.error('The DPD thermostat is not supported on CPU, please '
'choose a GPU host or a different thermostat.\n')
return None
if not msutils.is_coarse_grain(self._model):
self.error('DPD is a valid thermostat only for coarse-grained '
'models.\n')
return None
cfg_fname = self._write_cfg(jobname)
msj_str = self._get_msj(cfg_fname)
if (msj_str):
msj_fname = self._write_msj(jobname, msj_str)
cms_fname = self._write_cms(jobname)
if not os.path.isfile(cms_fname):
raise RuntimeError("File does not exist: %s" % cms_fname)
my_macro_dict = {"$JOBNAME": jobname}
if self.is_replica_exchange:
my_macro_dict["$REPLICA"] = 0
out_fname = sea.get_val(my_macro_dict, self._key.maeff_output.name)
# Turn on umbrella mode for all jobs
other_options = [
"-mode",
"umbrella",
]
if self.is_replica_exchange:
if (cpus * n_replica > total_cpus):
# Use time slicing.
other_options.extend([
"-set",
"stage[1].set_family.remd.total_proc=%d" % total_cpus,
])
# For replica exchange panel, the -cpus option always needs to
# be set to 1. The total # of processors should be specified
# via -HOST option.
# With CPUs, the number of procs should be a multiple of number
# replicas; With GPUs, it's opposite - multiple replicas can
# be run on a single GPU, so number replicas should be a
# multiple of number of processors.
host = "%s:%i" % (host, total_cpus)
# For replica exchange panel, the -cpus needs to be set to 1.
# (in the future we will deprecate this option completely)
cpus = 1
return self._submit_multisim_job(jobname,
incorp,
host,
None,
cpus,
maxjob,
cms_fname,
cfg_fname,
msj_fname,
out_fname,
other_options,
really_start=really_start)
def _write_msj(self, jobname, msj_str):
msj_fname = jobname + ".msj"
fh = open(msj_fname, "w")
print(msj_str, file=fh)
fh.close()
return msj_fname
def _write_cms(self, jobname):
cms_fname = jobname + ".cms"
if (self._model):
self._model.write(cms_fname)
return cms_fname
def _write_cfg(self, jobname):
"""
self is MDApp self.sim is MdGroup
"""
# Here make sure all options on the panel are updated
# before writing out
for w in self.sim._widgets:
w.updateKey(self._key)
cfg_fname = jobname + ".cfg"
key = copy.deepcopy(self._key)
if (key.ensemble.class_.val != "NPT"):
key["pressure"] = key.pressure[0].val
config.tag_sim_map(key, self._tag_spec, tag="output")
for backend_name in ['mdsim', 'vrun', 'remd', 'minimize']:
if backend_name in key.backend and \
'last_time' in key.backend[backend_name]:
key.backend[backend_name].last_time.val = key['time'].val
# delete fep block from frontend config file
# If the job is running under multisim, that block will be
# removed automatically. However, if it runs by desmond startup
# script directly, it will cause translation problem.
# Removing it before writing out the config file will solve the
# problem.
for del_key in ['fep', 'coulomb_method']:
if del_key in key:
del key[del_key]
fh = open(cfg_fname, "w")
print(key.__str__(tag='output'), file=fh)
fh.close()
return cfg_fname
[docs] def getNonMembraneRelaxationProtocol(self):
"""
:return: A template file path for a non-membrane relaxation protocol
:rtype: str
"""
pfile = self.sim.relx.getProtFilename()
self.sim.ens.updateKey(self._key)
ens = self._key.ensemble.class_.val
if not pfile:
if ens in [
"NPT",
"NPAT",
"NPgT",
]:
pfile = get_msj_template_path("desmond_npt_relax.msj")
else:
pfile = get_msj_template_path("desmond_nvt_relax.msj")
return pfile
[docs] def setUpMembraneRelaxation(self):
"""
Set up the system for membrane relaxation and return a path
to the membrane relaxation msj template.
:return: Path to membrane relaxation msj template.
:rtype: str
"""
# Membrane relaxation (PANEL-2251)
# Add energy group property to water molecules that are not
# crystallographic (if relaxing the membrane):
# This code is similar to relax_membrane.py
for ct in self._model.get_solvent_cts():
for a in ct.atom:
# add energy group property to non-xtal water molecules
if not a.property.get(constants.CRYSTAL_WATER_PROP):
a.property[constants.FEP_ABSOLUTE_ENERGY] = 1
# Now get contents for msj file from the template:
return os.path.join(envir.CONST.MMSHARE_DATA_DESMOND_DIR,
'desmond_membrane_relax.msj.template')
[docs] def getRelaxationStageStringFromTemplate(self, pfile, cfg_fname):
"""
Load a template relaxation protocol.
:param pfile: Template protocol file path
:type pfile: str
:param cfg_fname: Config filename to include in the protocol
:type cfg_fname: str
:return: A relaxation stage protocol string loaded from the specified template.
:rtype: str
"""
return cmj.append_stage(pfile, "simulate", cfg_fname, "$MAINJOBNAME",
".", "")
[docs] def getNoRelaxationProtocol(self, cfg_fname):
"""
Get msj string when no relaxation protocol will be run.
:param cfg_fname: Config filename to be included in the protocol
:type cfg_fname: str
:return: No-relaxation protocol
:rtype: str
"""
s = 'task { task = "desmond:auto" }\n'
s += 'simulate { cfg_file = "%s" jobname = "$MAINJOBNAME" dir = "." compress = "" checkpt.write_last_step = yes }' % cfg_fname
return s
def _get_msj(self, cfg_fname):
if self.sim.relx.shouldRelax():
if self._model.get_membrane_cts():
pfile = self.setUpMembraneRelaxation()
else:
pfile = self.getNonMembraneRelaxationProtocol()
s = self.getRelaxationStageStringFromTemplate(pfile, cfg_fname)
if s is None:
self.warning("Relaxation protocol not found, or parsing it "
"failed.\n\n%s." % pfile)
return
if self._model.get_membrane_cts():
s = update_membrane_relaxation_protocol(s)
# Replace TEMPERATURE placeholder in template with actual temp:
temperature = float(self.sim.ens.temp_ef.text())
s = s.replace('$TEMPERATURE', str(temperature))
else:
s = self.getNoRelaxationProtocol(cfg_fname)
return s
[docs] def readActionSelected(self):
"""
Called when the "Read" action is selected by the user. It's the
responsibility of individual panels to hook this method to the "Read"
menu action. See Ev:109801
"""
filetypes = "Desmond Config Files (*.cfg);;All Files (*)"
fname = filedialog.get_open_file_name(
self, self.title + " - Load Config File", '', filetypes)
if fname:
self.readCommand(str(fname))
# self.bottom_bar.read_bn.setEnabled(False)
# FIXME: The "Read" action can't be disabled in AF2.
[docs] @af2.appmethods.write("Write")
def writeCommand(self):
with wait_cursor:
self.startCommand(really_start=False)
[docs] def readCommand(self, fname):
if (fname):
if (fname[-4:] != ".cfg"):
self.warning("Unknown extension name for a config file.\n")
return
try:
with open(fname, "r") as fh:
s = fh.read()
except IOError:
error_dialog(self, "Reading file failed: '%s'")
return
cfg = sea.Map(s)
# FIXME: This is a bit awkward: We have to add the "annealing"
# parameter and set it to False if it is missing before the
# validity check. This is because there is a dependency of
# temperature checking on the value of the "annealing" parameter.
if ("annealing" not in cfg):
cfg["annealing"] = False
cfg_validator = sea.Map("")
cfg_validator["DATA"] = cfg
cfg_validator["VALIDATE"] = self._validator
ev = sea.Evalor(cfg_validator)
sea.check_map(cfg_validator.DATA, cfg_validator.VALIDATE, ev)
err = ev.err
if (err):
self.warning("The following errors are found for the config "
"file:\n\n" + err)
return
if (not self.isKeyConsistentWithPanel(cfg)):
self.warning("The config file is not for %s." % self.title)
return
self._key.update(config.canonicalize(cfg))
self.updateFromKey(self._key)
if (self._model):
for w in self.sim._widgets:
if (hasattr(w, "model_changed_callback")):
w.model_changed_callback(self._model)
[docs] def isKeyConsistentWithPanel(self, key):
print('WARNING: Must overwrite isKeyConsistentWithPanel()')
return True
# Needed for the "Reset" action to appear
[docs] @af2.appmethods.reset('Reset')
def reset(self):
pass
[docs] @wait_cursor
def setDefaults(self):
"""
Resets the parameters to their default values.
"""
Super.setDefaults(self)
self._key = copy.deepcopy(self._default_key)
self.updateFromKey(self._key)
self.model_sys.reset()
for w in self.sim._widgets:
w.updateFromKey(self._key)
# Reset the advanced dialog (options that are not based on the key):
if hasattr(self.sim, "adv_frame") and self.sim.adv_frame.dialog:
self.sim.adv_frame.dialog.reset()
[docs] def closeEvent(self, event):
"""
Hide panel, if in Maestro. Otherwise close it.
"""
if self.sim.adv_frame.dialog:
if maestro:
self.sim.adv_frame.hideDialog()
else:
self.sim.adv_frame.closeDialog()
Super.closeEvent(self, event)
[docs]class DesmondGuiConfigDialog(ConfigDialog):
"""
This Config Dialog allows toggling between CPU and GPU hosts.
"""
[docs] def __init__(self,
parent,
title="",
jobname="",
checkcommand=None,
multi_gpgpu_allowed=True,
no_cpu=True,
**kw):
self.multi_gpgpu_allowed = multi_gpgpu_allowed
self.proc_combo = QtWidgets.QComboBox()
self.gpu_not_avail_label = QtWidgets.QLabel("(GPU not available)")
self.gpu_not_avail_label.setVisible(False)
super().__init__(parent, title, jobname, checkcommand, **kw)
avail_units = [GPU] if no_cpu else [CPU, GPU]
self.proc_combo.addItems(avail_units)
self.proc_combo.setFixedWidth(65)
self.proc_combo.currentIndexChanged.connect(
lambda: self.onProcUnitComboIndexChanged())
proc_selector_layout = QtWidgets.QHBoxLayout()
proc_label = QtWidgets.QLabel("Processing unit:")
proc_selector_layout.addWidget(proc_label)
proc_selector_layout.addWidget(self.proc_combo)
layout = self.main_host_layout
layout.insertLayout(0, proc_selector_layout)
if no_cpu:
proc_label.setVisible(False)
self.proc_combo.setVisible(False)
else:
spacer = QtWidgets.QSpacerItem(200, 20,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum)
proc_selector_layout.addItem(spacer)
self.host_menu_layout.insertWidget(2, self.gpu_not_avail_label)
self.onProcUnitComboIndexChanged()
[docs] def onWriteRequested(self):
"""
Slot for Write button.
"""
# Skip host platform check for writing files
if not super().validate():
return
self.requested_action = config_dialog.RequestedAction.Write
self.dialog.accept()
[docs] def validateNumProcs(self, silent=False):
"""
See ConfigDialog.validateNumProcs docstring.
"""
host = self.currentHost()
if host.hostType() == config_dialog.Host.CPUTYPE:
# The cpu entry field might be blank because it isn't used for
# this type of dialog (implicitly 1 - which is always OK).
if self.num_cpus_sb.value():
if not self.validateNumCpus(host, self.num_cpus_sb, silent):
return False
if host.hostType() == config_dialog.Host.GPUTYPE:
if not self.validateNumGpus(host, self.num_cpus_sb, silent):
return False
return True
[docs] def validate(self):
host = self.currentHost()
if not platforms.validate_host(host):
self.warning(platforms.PLATFORM_WARNING)
return False
return ConfigDialog.validate(self)
[docs] def getSettings(self, extra_kws=None):
"""
Return dialog state by saving the state of the checkbox and then
calling the base class
"""
if extra_kws is None:
kw = {}
else:
kw = extra_kws
# Whether the select host has GPUs:
proc_units = self.proc_combo.currentText()
kw[self.last_proc_units_prefkey] = proc_units
self._app_preference_handler.set(self.last_proc_units_prefkey,
proc_units)
return ConfigDialog.getSettings(self, extra_kws=kw)
[docs] def applySettings(self, settings):
"""
See parent class docstring
"""
if settings is None:
return
proc_units = settings.proc_units or self._app_preference_handler.get(
self.last_proc_units_prefkey, CPU)
self.proc_combo.setCurrentText(proc_units)
use_host = self.getHostPref()
self.onProcUnitComboIndexChanged(use_host=use_host)
# NOTE: "gpus" option is ignored - the config dialog will always
# determine whether selected hosts have GPUs based on the gpu_list
# attribute of the selected host.
return ConfigDialog.applySettings(self, settings)
[docs] def setupHostLayout(self):
can_start = super().setupHostLayout()
labelwidth = self.cpus_units_label.sizeHint().width()
self.cpus_units_label.setFixedWidth(labelwidth)
if self.options['save_host']:
all_hosts = [
h.label() for h in config_dialog.get_hosts(excludeGPGPUs=False)
]
cpu_hosts = [
h.label() for h in config_dialog.get_hosts(excludeGPGPUs=True)
]
use_host = self._app_preference_handler.get(self.last_host_prefkey,
None)
if use_host and use_host not in cpu_hosts:
if not (use_host in all_hosts or
(len(cpu_hosts) == len(all_hosts) and
config_dialog.DUMMY_GPU_HOSTNAME in use_host)):
return can_start
gpu_idx = self.proc_combo.findText(GPU)
self.proc_combo.setCurrentIndex(gpu_idx)
idx = self.host_menu.findText(use_host)
if idx > -1:
self.host_menu.setCurrentIndex(idx)
return can_start
[docs] def getHosts(self, _1=True, _2=False):
"""
Get the hosts based on current CPU/GPU setting.
:return: List of current hosts
:rtype: `config_dialog.Host`
"""
proc_type = self.proc_combo.currentText()
use_cpus = proc_type == CPU
hosts = config_dialog.get_hosts(excludeGPGPUs=use_cpus)
if not use_cpus:
hosts = [
h for h in hosts if h.hostType() == config_dialog.Host.GPUTYPE
]
if not hosts:
hosts = [config_dialog.DummyGpuHost()]
return hosts
[docs] def onProcUnitComboIndexChanged(self, use_host=None):
"""
Update the available hosts based on the current processor unit type.
Will also hide/show widgets based on whether GPU is selected but no
GPU hosts are available or not.
:param use_host: Host to be set in the host combo
:type use_host: str
"""
is_gpu = self.proc_combo.currentText() == GPU
units = self.GPU_UNIT_LABEL if is_gpu else self.CPU_UNIT_LABEL
self.cpus_units_label.setText(units)
self.hosts = self.getHosts()
self.setupHostCombo(self.host_menu, use_host=use_host)
can_start = not isinstance(self.currentHost(),
config_dialog.DummyGpuHost)
show_procs = self.options['cpus'] is True
enable = show_procs and (self.multi_gpgpu_allowed or not is_gpu)
for wid_str in ['cpus_label', 'num_cpus_sb', 'cpus_units_label']:
if hasattr(self, wid_str):
wid = getattr(self, wid_str)
wid.setVisible(can_start and show_procs)
wid.setEnabled(enable)
if not self.multi_gpgpu_allowed and show_procs and is_gpu:
self.num_cpus_sb.setValue(1)
self.gpu_not_avail_label.setVisible(not can_start)
for wid in ['incorp_menu', 'host_menu']:
if hasattr(self, wid):
getattr(self, wid).setEnabled(can_start)
self.start_button.setEnabled(can_start)
self.start_button.setAutoDefault(can_start)
if not can_start:
self.write_button.setAutoDefault(True)
[docs]class SingleGpuDesmondGuiConfigDialog(DesmondGuiConfigDialog):
[docs] def __init__(self, *args, **kwargs):
super().__init__(*args, multi_gpgpu_allowed=False, **kwargs)
[docs]class DesmondRestGuiConfigDialog(DesmondGuiConfigDialog):
[docs] def __init__(self, parent, title="", jobname="", checkcommand=None, **kw):
super().__init__(parent, title, jobname, checkcommand, **kw)
# EOF