Source code for schrodinger.application.desmond.gui

# -*- 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] def headerData(self, section, orientation, role): """ Returns the string that should be displayed in the specified header cell. Used by the View. """ if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return section + 1 elif orientation == Qt.Vertical: return self.row_header[section]
[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
[docs] def headerData(self, section, orientation, role): """ Returns the string that should be displayed in the specified header cell. Used by the View. """ if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.header[section] elif orientation == Qt.Vertical: return section + 1 elif role == Qt.ToolTipRole: if orientation == Qt.Horizontal: tooltip = self.header_tooltips[section] if tooltip: return tooltip
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 updatePosresButtons(self, ignored1=None, ignored2=None): selcount = len(self.posres_view.selectionModel().selectedRows()) self.btn_select.setEnabled(selcount > 0) self.btn_delete.setEnabled(selcount > 0) self.btn_reset.setEnabled(self.posres_model.rowCount() > 0)
[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] def headerData(self, section, orientation, role): """ Returns the string that should be displayed in the specified header cell. Used by the View. """ if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.header[section] elif orientation == Qt.Vertical: return section + 1
[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 updateAtomGroupButtons(self, ignored1=None, ignored2=None): selcount = len(self.atomgroup_view.selectionModel().selectedRows()) self.btn_select.setEnabled(selcount > 0) self.btn_delete.setEnabled(selcount > 0) self.btn_reset.setEnabled(self.atomgroup_model.rowCount() > 0)
[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 InputGroup(object): """ - Group Frame for showing the widgets for loading the system model. - Used by the `GuiApp` class for all panels. """
[docs] def __init__(self, master, model_changed_callback, allow_checkpoint_files=True, check_infinite=False, reset_parent_on_load=True, cg_aware=False): """ See class docstring. :param master: The root or master application :type master: `af2.JobApp` object. :type check_infinite: bool :param check_infinite: If a checkbox for infinite systems should be included and if the system should be checked for infinite bonding when loaded :param reset_parent_on_load: Whether or not the InputGroup should call it's parent widget's setDefaults() function when loading a new model system. :type reset_parent_on_load: bool :param cg_aware: Whether or not the InputGroup should check if the model is coarse-grained (and what type) or not :type cg_aware: bool """ self.app = master self.ui = input_group_ui.Ui_Form() self.widget = QtWidgets.QWidget() self.ui.setupUi(self.widget) self.group_box = self.ui.model_system_group_box self._model_changed_callback = model_changed_callback self._sys_type = constants.SystemType.OTHER self._reset_parent_on_load = reset_parent_on_load self.cg_aware = cg_aware self.modelsource_menu = self.ui.load_model_from_combo_box self.modelsource_menu.currentIndexChanged.connect( self._model_source_changed) self.load_button = self.ui.load_button self.load_button.clicked.connect(self._import_from_workspace) #TODO: See code review 14023 supported_files = ';;'.join([ 'All supported files (*cms *cpt *cms.gz *cmsgz)', 'Desmond checkpoint files (*cpt)', 'Chemical model system files (*cms *cms.gz *cmsgz)', 'All files (*)', ]) if not allow_checkpoint_files: supported_files = 'Chemical model system files (*cms *cms.gz *cmsgz)' self.file_ef = cwidget.FileEntry(self.group_box, "", "Desmond - Open Model System File", supported_files, command=self.browseSystemFile) self.file_ef.hide() self.info = self.ui.system_status_label # Infinitely bonded systems self.check_infinite = check_infinite # TO DO: Remove infinite_cb widget from ui self.ui.infinite_cb.setChecked(False) self.ui.infinite_cb.hide() # Structure house-keeping data self.model = None # An `cms.Cms' object # If it is a.cpt file, we record its file name here. self.cpt_fname = None self.in_cms_fname = None self._model_source_changed(self.modelsource_menu.currentIndex())
# __init__ def _analyse_model(self): """ Called when a new system is analyzed (from file or Workspace) """ if self._reset_parent_on_load: self.app.setDefaults() # Is this model compatible with the panel? self._sys_type = cms.get_model_system_type(self.model.comp_ct) if (self._sys_type == constants.SystemType.ALCHEMICAL): error_dialog( self, "The model system was set up for FEP simulations.\n" "You cannot run non-FEP simulation with this system.") return 1 process_loaded_model(self.model) return 0 def _sync_workspace_atom_props(self): """ The model associated with this class is always untethered from the WS structure, even if it was loaded from the workspace. This function attempts to assign the Desmond "scratch" atom-level properties to the workspace structure based on their values in the model's component CTs. """ workspace_st = maestro.workspace_get(copy=False) ws_atom_comp_map = {} # Generate a map of Desmond components to WS atoms. for atom in workspace_st.atom: comp_type = atom.property.get(constants.CT_TYPE) if not comp_type: # The workspace has non-relevant atoms present. Do nothing. return if comp_type in ws_atom_comp_map: ws_atom_comp_map[comp_type].append(atom.index) else: ws_atom_comp_map[comp_type] = [atom.index] # Now check that the comp CT atom counts are equal to those we extracted. for ct in self.model.comp_ct: ct_type = ct.property.get(constants.CT_TYPE) if (ct_type not in ws_atom_comp_map or len(ws_atom_comp_map[ct_type]) != ct.atom_total): return # Now that all components have been verified against the WS, we can sync the props. for ct in self.model.comp_ct: comp_type = ct.property[constants.CT_TYPE] for atom in ct.atom: for prop in [ DES_SCRATCH_ENTRY_NUM_PROP, DES_SCRATCH_ATOM_NUM_PROP, DES_FULLSYS_ID_PROP ]: comp_val = atom.property.get(prop) if comp_val is not None: # atom.index is 1 based so we need to subtract 1 for our 0 based array. ws_atom_idx = ws_atom_comp_map[comp_type][atom.index - 1] workspace_st.atom[ws_atom_idx].property[prop] = comp_val def _postprocess_after_importing(self): """ Used by all panels to import results. """ if (self.model): ending = 'atoms' if self.cg_aware: cgtype = coarsegrain.get_nonbond_potential_type(self.model) if cgtype == coarsegrain.DISSIPATIVE_PARTICLE: ending = 'coarse-grained repulsive harmonic particles' elif cgtype == coarsegrain.LENNARD_JONES: ending = 'coarse-grained Lennard-Jones particles' elif cgtype == coarsegrain.SHIFTED_LENNARD_JONES: ending = 'coarse-grained shifted Lennard-Jones particles' num_atom = len(self.model.atom) text = "The system contains %d %s." % (num_atom, ending) if maestro: maestro.command("fit") else: text = "This is a Desmond check-point file." self.info.setText(text) if hasattr(self.app, 'enableSimulationGroupBox'): self.app.enableSimulationGroupBox(True) self._model_changed_callback(self.model) if self.model and self.model.title: if len(self.model.title) > 70: title = self.model.title[:67] + '...' else: title = self.model.title text = str(self.info.text()).replace('The system', title) # PANEL-7837 If input is a trajectory, let the user know that # the last frame will be used as the starting point: if self.model.number_of_frames > 0: text += '\nThe last trajectory frame (%i) will be used as input for the new job.' % self.model.number_of_frames self.info.setText(text) if self.app.sim is not None: is_cpt = self.app._is_cpt if hasattr(self.app.sim, 'adv_frame'): adv_dialog = self.app.sim.adv_frame.dialog if is_cpt: adv_dialog.adv_ui.rand_gb.setChecked(False) adv_dialog.adv_ui.rand_gb.setEnabled(not is_cpt) if hasattr(self.app.sim, 'relx'): if is_cpt: self.app.sim.relx.relax_box.setChecked(False) self.app.sim.relx.relax_box.setEnabled(not is_cpt)
[docs] def isInfiniteModel(self): """ Is the current model marked as an infinitely bonded model? :rtype: bool :return: Whether the model is marked as infinitely bonded """ if self.model: return bool(self.model.property.get(constants.IS_INFINITE, False)) return False
def _model_source_changed(self, index=None): """ Used by all panels to update the model source. """ if index == 0: self.ui.input_row_layout.insertWidget(1, self.load_button) self.load_button.show() self.ui.input_row_layout.removeWidget(self.file_ef) self.file_ef.hide() else: self.ui.input_row_layout.removeWidget(self.load_button) self.load_button.hide() self.ui.input_row_layout.addWidget(self.file_ef, 1) self.file_ef.show() def _import_from_workspace(self): """ Used by all panels to import from the workspace """ # Call sequence: _import_from_workspace -> import_from_workspace_impl # -> _analyse_model -> _postprocess_after_importing with wait_cursor: out = self._import_from_workspace_impl() if out: self.info.setText("The system is not specified.") return self._postprocess_after_importing() self.file_ef.entry.clear() def _import_from_workspace_impl(self): # Find the entry ID of the system Workspace entry fsys_eid = None structs, eid = cmae.Workspace().grab() # FIXME Do not show wait cursor when an error dialog is displayed. if (len(structs) == 0): error_dialog(self, "No structure in the Workspace.") return 1 if (len(structs) > 1): error_dialog(self, "There must be only one structure in the Workspace.") return 1 try: cttype = structs[0].property[constants.CT_TYPE] except KeyError: error_dialog( self, "Structure in the Workspace is not a model system.\n\n" "You may use the System Builder panel to create a model " "system for this structure.") return 1 if cttype == constants.CT_TYPE.VAL.FULL_SYSTEM: fsys_eid = eid[0] else: error_dialog( self, "Structure in the Workspace is not a full model system.\n\n" "You may use the System Builder panel to create a model " "system for this structure.") return 1 pt = maestro.project_table_get() row = pt[fsys_eid] cms_file = row.cms_file if (cms_file is None): error_dialog( self, "The \"s_m_original_cms_file\" property is not " "defined in the model system.") return 1 try: self.model = cms.Cms(cms_file) except OSError: error_dialog( self, 'The original cms file could not be ' 'located: \n%s' % cms_file) return 1 # Maestro has set this property to the absolute path of the original # trajectory directory when the CMS file was imported into the PT. # (the CMS file is located in project's additional data dir). trj_dir = row['s_chorus_trajectory_file'] self.model.number_of_frames = count_traj_frames(trj_dir) # Sync the row with the newly loaded .cms # We need to do this so that the WS structure # can be synced w/ the model. success_code = self._analyse_model() if success_code == 0: # Need to reset the row's structure and exclude/reinclude to # make sure that the atom level properties are available in # the WS structure. CT-level properties are not copied, as the # PT entry may contain extra properties that are not present # in the original CMS file, like s_m_original_cms_file. # See PANEL-7679 row.setStructure(self.model, props=False) self._sync_workspace_atom_props() return success_code
[docs] def browseSystemFile(self, fname): # Call sequence: browseSystemFile -> import_from_file_impl -> # _analyse_model -> _postprocess_after_importing if not fname: return with wait_cursor: out = self._import_from_file_impl(fname) if out: self.info.setText("The system is not specified.") else: self._postprocess_after_importing()
def _import_from_file_impl(self, fname): # Clears atom selection on the current Workpace in order to avoid the # "WARNING is_on/off()" warnings. if maestro: maestro.command("workspaceselectionreplace atom.num 0") if (fname[-4:] not in [ ".cms", ".cpt", ] and fname[-7:] != ".cms.gz" and fname[-6:] != ".cmsgz"): error_dialog( self, "Unknown file extension name.\n\n" "Supported extension names are.cms,.cms.gz,.cmsgz, " "and.cpt.") return 1 if (fname[-4:] == ".cpt"): return self._import_cpt(fname) if (not os.path.isfile(fname)): error_dialog(self, "File not found:\n\n\"" + fname + "\"") return 1 try: self.model = cms.Cms(file=fname) except Exception as e: error_dialog(self, str(e)) return 1 import schrodinger.application.desmond.packages.topo as topo trj_dir = topo.find_traj_path_from_cms_path(fname) self.model.number_of_frames = count_traj_frames(trj_dir) success_code = self._analyse_model() if maestro and success_code == 0: # Include this model into the Workspace: pt = maestro.project_table_get() row = pt.importStructure(self.model, name=self.model.title, wsreplace=True) self._sync_workspace_atom_props() # Make sure that the PDB and Chorus lattice params are in sync # (MATSCI-4508). This has to be done after import into the PT # because Maestro actually reads the properties from the CMS file. struct = row.getStructure() xtal.sync_pbc(struct, in_place=True) # Calling row.setStructure(struct) removes the atom-level props # set above via _sync_workspace_atom_props, which modifies the # Workspace CT but not the Project CT. Using setStructure() here # will push the project CT onto the Workspace, erasing these # properties in the process. # Instead we sync the property dicts. for propkey, propval in struct.property.items(): if propkey == 's_m_entry_id': continue row[propkey] = propval return success_code def _import_cpt(self, fname): cfg = config.extract_cfg(fname) with open(fname, 'rb') as f: # check if -in.cms exists because driver looks for -in.cms to # write -out.cms in_cms_fname = get_in_cms_from_cpt(fname, cfg=cfg) if not os.path.isfile(in_cms_fname): error_dialog( self, "Please place %s in the same directory as %s" % (in_cms_fname, fname)) return 1 self.in_cms_fname = in_cms_fname if b'gdesmond' in f.read(): self.app.gpu = True else: self.app.gpu = False if (cfg is None): error_dialog(self, "Parsing the.cpt file failed.") return 1 self.model = None self.cpt_fname = fname self._key = config.canonicalize(cfg) # PANEL-7767 Do not check "Randomize Velocities" for CPT self._key.randomize_velocity.first.val = 'inf' return 0
[docs] def reset(self): """ Resets the input frame to defaults """ if hasattr(self.app, 'enableSimulationGroupBox'): self.app.enableSimulationGroupBox(True) self.modelsource_menu.setCurrentIndex(0) self.file_ef.setText("") self.info.setText("The system is not specified.") self.ui.infinite_cb.setChecked(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 enablePlatformValidation(self, state): """ Enable/disable platform validation of localhost jobs :type state: bool :param state: Whether to use platform validation or not """ self.validate_platforms = state
[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 setUpButtonBox(self, can_start=True): """ Set up the dialog's button box and add 'Write' button. """ self.desmond_write_button = QtWidgets.QPushButton(ConfigDialog.WRITE) if can_start: self.button_box.addButton(self.desmond_write_button, QtWidgets.QDialogButtonBox.ActionRole) self.desmond_write_button.clicked.connect(self.onWriteRequested) super().setUpButtonBox(can_start=can_start)
[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