Source code for schrodinger.application.matsci.mswidgets

"""
Contains widgets that are useful in MatSci panels.

Copyright Schrodinger, LLC. All rights reserved.
"""

import inflect
import math
import re
import os
from collections import OrderedDict, namedtuple
from contextlib import contextmanager

import numpy
from scipy import ndimage

import schrodinger
from schrodinger import project
from schrodinger import structure
from schrodinger.application.bioluminate import sliderchart
from schrodinger.application.desmond import constants as dconst
from schrodinger.application.jaguar import input as jagin
from schrodinger.application.jaguar.gui import utils
from schrodinger.application.jaguar.gui.tabs import solvation_tab
from schrodinger.application.matsci import parserutils
from schrodinger.application.matsci.nano import xtal
from schrodinger.application.matsci import jobutils
from schrodinger.graphics3d import common as graphics_common
from schrodinger.graphics3d import arrow
from schrodinger.graphics3d import polygon
from schrodinger.infra import mm
from schrodinger.infra import mmcheck
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.structutils import analyze
from schrodinger.structutils import measure
from schrodinger.structutils import transform
from schrodinger.ui import picking
from schrodinger.ui.qt import utils as qtutils
from schrodinger.ui.qt import appframework
from schrodinger.ui.qt import atomselector
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import multi_combo_box
from schrodinger.ui.qt import swidgets
from schrodinger.utils import units

from . import controlicons_rc  # pylint: disable=unused-import
from . import desmondutils
from . import jaguarworkflows
from . import msutils

NO_SOLVENT = 'None'
# Jaguar 'solvent' keyword
SOLVENT_KEY = mm.MMJAG_SKEY_SOLVENT
# Jaguar 'isolv' keyword
MODEL_KEY = mm.MMJAG_IKEY_ISOLV

# Names of icons used for stage control buttons
DOWN = 'down'
UP = 'up'
CLOSE = 'close'
OPEN = 'open'
DELETE = 'delete'
COPY = 'copy'

SAVE_COMBO_OPTIONS = OrderedDict()
SAVE_COMBO_OPTIONS['CMS files'] = jobutils.SAVE_CMS
SAVE_COMBO_OPTIONS['CMS and trajectory archive'] = jobutils.SAVE_TRJ

NUM_RE = re.compile(r'(\d+)')

LATTICE_VECTOR_LABELS = ['a', 'b', 'c']
MINIMUM_PLANE_NORMAL_LENGTH = 2.0
INFLECT_ENGINE = inflect.engine()

maestro = schrodinger.get_maestro()





[docs]def sub_formula(formula): """ Add <sub></sub> tags around numbers. Can be used to prettify unit cell formula. :type formula: str :param formula: Formula to add tags around numbers :rtype: str :return: Formula with tags added """ return NUM_RE.sub(r'<sub>\1</sub>', formula)
[docs]def center_widget_in_table_cell(table, row, column, widget, layout_margins=()): """ Centers the widget in the cell and adds a non-selectable widget item so the cell background can't be selected :type table: QTableWidget :param table: The table that contains the cell & widget :type row: int :param row: The row of the cell :type column: int :param column: The column of the cell :type widget: QWidget :param widget: The widget that will be placed into the cell and centered :param tuple layout_margins: Tuple containing left, top, right, and bottom margins to use around the layout """ centering_widget = QtWidgets.QWidget() layout = swidgets.SHBoxLayout(centering_widget) layout.setAlignment(Qt.AlignCenter) if layout_margins: layout.setContentsMargins(*layout_margins) layout.addWidget(widget) table.setCellWidget(row, column, centering_widget) # Add a non-selectable Item so the user can't accidently select this cell item = QtWidgets.QTableWidgetItem() item.setFlags(Qt.NoItemFlags) table.setItem(row, column, item)
[docs]def get_row_from_pt(maestro, entry_id): """ Get row from the PT from entry_id :param str entry_id: Entry ID :param maestro maestro: Maestro instance :rtype: ProjectRow or None :return: Row or None, if not found """ ptable = maestro.project_table_get() try: row = ptable.getRow(entry_id) except (ValueError, TypeError): return return row
[docs]class MdAtomSelector(atomselector.AtomSelector): """ Overide the AtomSelector class in atomselector. Modify the reset button to clear asl text only, and add a status label if needed. """ ASL_NOT_ALLOWED = ['entry.id', 'entry.name', 'set']
[docs] def __init__(self, master, title, status_label=True, command=None, flat=False): """ See the parent class for documentation :param QWidget master: The parent of this atomselector widget :param str title: label over atom selection box :param bool status_label: If True, add a status label to indicate the number of selected atoms. :type command: function or None :param command: Command to execute when the asl is modified. Signal is triggered only when editing finishes. :param bool flat: Apply flat style to the groupbox, by default it is false in qt. """ super().__init__(master, label=title, show_plus=True) self.master = master if status_label: self.status_label = swidgets.SLabel("0 atoms selected") self.main_layout.insertWidget(0, self.status_label) else: self.setStyleSheet(self.styleSheet() + ("QGroupBox{padding-top:1em; margin-top:-1em}")) self.pick_toggle.setChecked(True) self.asl_ef.getClearButton().clicked.disconnect() self.asl_ef.getClearButton().clicked.connect(self.clearAslText) self.setFlat(flat) self.command = command self.aslModified.connect(self.checkASL)
[docs] def clearAslText(self): """ Currently, the reset button is used to clear asl string """ self._setAsl('')
[docs] def pickMolecule(self): """ Set the pick_menu with pick molecule option. """ self.pick_menu.setCurrentIndex(atomselector.PICK_MOLECULES)
[docs] def checkASL(self, asl): """ Check if asl contains sets, entry.id or entry.name :param str asl: asl in the line edit """ entry_in_asl = [x for x in self.ASL_NOT_ALLOWED if x in asl] if entry_in_asl: self.master.error(f'{entry_in_asl[0]} as ASL is not supported.') self._setAsl('') if self.command: self.command()
[docs]class CompactSolventSelector(swidgets.SFrame): """ A single line of widgets that displays the currently chosen solvent and a button that will open a dialog allowing a new solvent model/solvent choice. Tracks the necessary Jaguar keywords to implement the user's choice. """
[docs] def __init__(self, parent=None, layout=None, indent=False, keywords=None, **extra_args): """ Create a CompactSolventSelector object Additional keyword arguments are passed on to the SolventDialog that is opened by this widget. :type layout: QBoxLayout :param layout: The layout to place this widget into :param bool indent: If true indent frame layout :type keywords: dict :param keywords: Dictionary of solvent-related Jaguar key/value pairs to initialize/reset the widgets with """ super().__init__( parent=parent, layout=layout, indent=indent, layout_type=swidgets.HORIZONTAL) self.other_solvent_options = {} if keywords is None: self.keywords = {} else: self.keywords = keywords self.default_keywords = self.keywords.copy() # Translates solvent keyword names to solvent user names self.mmname_to_name = dict( [(x.keyvalue, x.name) for x in utils.ALL_SOLVENTS]) self.label_label = swidgets.SLabel('Solvent:', layout=self.mylayout) sname = self.getSolventName() self.solvent_label = swidgets.SLabel(sname, layout=self.mylayout) self.choose_btn = swidgets.SPushButton( 'Choose...', command=self.chooseSolvent, layout=self.mylayout) self.dialog_args = extra_args self.mylayout.addStretch()
[docs] def chooseSolvent(self): """ Open a dialog that lets the user choose solvent parameters (model, solvent, solvent properties) and store the choices """ dialog = SolventDialog(self, keywords=self.keywords, **self.dialog_args) dialog.keywordsChanged.connect(self.solventKeywordsChanged) dialog.exec_()
[docs] def solventKeywordsChanged(self, keywords, options={}): # noqa: M511 """ Called when the user clicks accept on the SolventDialog :type keywords: dict :param keywords: A dictionary of Jaguar solvent model keywords """ self.keywords = keywords self.other_solvent_options = options self.solvent_label.setText(self.getSolventName())
[docs] def isSolventModelUsed(self): """ Has a solvent model been chosen? :rtype: bool :return: True if yes, False if no """ return bool(self.keywords.get(MODEL_KEY, 0))
[docs] def getSolventName(self): """ Get the name of the chosen solvent :rtype: str :return: The user-facing name of the chosen solvent, or NO_SOLVENT if no model has been chosen """ if self.isSolventModelUsed(): return self.mmname_to_name[self.keywords[SOLVENT_KEY]] else: return NO_SOLVENT
[docs] def getKeystring(self): """ Get a string containing all the keywords specified by the user's choices :rtype: str :return: A string containg keywords that define the user's choices. An empty string is returned if no model has been selected """ skeywords = "" if not self.isSolventModelUsed(): skeywords = "" else: skeywords = jaguarworkflows.keyword_dict_to_string(self.keywords) oskeywords = "" oskeywords = jaguarworkflows.keyword_dict_to_string( self.other_solvent_options) return (skeywords, oskeywords)
[docs] def reset(self): """ Reset all the widgets to their original values """ self.solventKeywordsChanged(self.default_keywords)
[docs]class SolventDialog(QtWidgets.QDialog): """ A Dialog that allows the user to pick a solvent model, solvent and parameters. Emits a keywordsChanged signal when the user clicks Accept and passes a dictionary of the Jaguar keywords that reflect the selected settings. """ keywordsChanged = QtCore.pyqtSignal(dict, dict)
[docs] def __init__(self, parent, keywords=None, **extra_args): """ Create a SolventDialog object Additional keyword arguments are passed to the EmbeddedSolventWidget object :type parent: QWidget :param parent: The parent widget for this dialog :type keywords: dict :param keywords: A dictionary of jaguar key/value pairs that define the initial solvent settings for the dialog """ self.master = parent QtWidgets.QDialog.__init__(self, parent) self.setWindowTitle('Solvent Model') layout = swidgets.SVBoxLayout(self) layout.setContentsMargins(6, 6, 6, 6) if keywords is None: self.keywords = {} else: self.keywords = keywords self.solvent_widgets = EmbeddedSolventWidget( keywords=self.keywords, **extra_args) layout.addWidget(self.solvent_widgets) dbb = QtWidgets.QDialogButtonBox dialog_buttons = dbb(dbb.Save | dbb.Cancel | dbb.Help) dialog_buttons.accepted.connect(self.accept) dialog_buttons.rejected.connect(self.reject) dialog_buttons.helpRequested.connect(self.help) layout.addWidget(dialog_buttons)
[docs] def help(self): """ Show the Jaguar solvent help """ appframework.help_dialog('JAGUAR_TOPIC_SOLVATION_FOLDER', parent=self)
[docs] def accept(self): """ Gather the options and emit a keyword dictionary with the keywords/values they define, then close the dialog. """ keywords = self.solvent_widgets.getMmJagKeywords() for key, value in list(keywords.items()): if value is None: del keywords[key] # The solvation tab no longer allows for "other" solvents, so there are # no other solvent options to emit options = {} self.keywordsChanged.emit(keywords, options) return QtWidgets.QDialog.accept(self)
[docs]class EmbeddedSolventWidget(solvation_tab.SolvationTab): """ A master widget that contains the widgets from the Jaguar Solvation tab and is convenient to use outside the Jaguar gui environment. """
[docs] def __init__(self, parent=None, layout=None, dielectric=True, reference=False, keywords=None, models=None): """ Create a EmbeddedSolventWidget object :type parent: QWidget :param parent: The parent widget for this widget :type layout: QBoxLayout :param layout: The layout to place this widget into :type dielectric: bool :param dielectric: If True, show the dielectric widgets, if False, do not :type reference: bool :param reference: If True, show the reference energy widgets, if False, do not :type keywords: dict :param keywords: The Jaguar solvent-related key/value pairs that define the initial widget values :type models: dict :param models: The allowed solvent models. keys are strings displayed to the user, values are Jaguar keywords. See parent class SOLVENT_MODELS constant for example. Use an OrderedDict to control the order of the solvent models in the model combobox """ if models: self.SOLVENT_MODELS = models self.SHOW_DIELECTRIC = dielectric self.reference = reference super().__init__(parent) if not self.reference: self.ui.gas_phase_frame.hide() self.layout().setContentsMargins(0, 0, 0, 0) if layout is not None: layout.addWidget(self) self.loadSettingsFromKeywords(keywords) self.default_keywords = keywords
def _getGasPhaseKeywords(self): """ Overrides the parent method to return an empty dictionary if the reference energy widgets are not shown """ if self.reference: return solvation_tab.SolvationTab._getGasPhaseKeywords(self) else: return {}
[docs] def solventModelChanged(self): super().solventModelChanged() if not self.reference: self.ui.gas_phase_frame.hide()
[docs] def loadSettingsFromKeywords(self, keywords): """ Set the widget states based on the given keyword dictionary :type keywords: dict :param keywords: Keys are jaguar keywords, values are keyword values """ jaginput = jagin.JaguarInput(genkeys=keywords) self.loadSettings(jaginput)
[docs] def reset(self): """ Reset the widgets to their initial values """ self.loadSettingsFromKeywords(self.default_keywords)
[docs]class WheelEventFilterer(QtCore.QObject): """ An event filter that turns off wheel events for the affected widget """
[docs] def eventFilter(self, unused, event): """ Filter out mouse wheel events :type unused: unused :param unused: unused :type event: QEvent :param event: The event object for the current event :rtype: bool :return: True if the event should be ignored (a Mouse Wheel event) or False if it should be passed to the widget """ return isinstance(event, QtGui.QWheelEvent)
[docs]def turn_off_unwanted_wheel_events(widget, combobox=True, spinbox=True, others=None): """ Turns off the mouse wheel event for any of the specified widget types that are a child of widget Note: The mouse wheel will still scroll an open pop-up options list for a combobox if the list opens too large for the screen. Only mouse wheel events when the combobox is closed are ignored. :type widget: QtWidgets.QWidget :param widget: The widget to search for child widgets :type combobox: bool :param combobox: True if comboboxes should be affected :type spinbox: bool :param spinbox: True if spinboxes (int and double) should be affected :type others: list :param others: A list of other widget classes that should be affected """ affected = [] if others: affected.extend(others) if combobox: affected.append(QtWidgets.QComboBox) if spinbox: affected.append(QtWidgets.QAbstractSpinBox) for wclass in affected: for child in widget.findChildren(wclass): child.installEventFilter(WheelEventFilterer(widget))
[docs]def run_parent_method(obj, method, *args): """ Try to call a function of a parent widget. If not found, parent of the parent will be used and so on, until there are no more parents. *args will be passed to the found function (if any). :param QWidget obj: QWidget to use :param str method: Method name to be called """ parent = obj.parentWidget() while parent is not None: if hasattr(parent, method): getattr(parent, method)(*args) return parent = parent.parentWidget()
[docs]class StageFrame(swidgets.SFrame): """ The base frame for a stage in a MultiStageArea Contains a Toolbutton for a title and some Window-manager-like control buttons in the upper right corner """ # Used to store the icons for buttons so they are only generated once. icons = {}
[docs] def __init__(self, master, layout=None, copy_stage=None, stage_type=None, icons=None): """ Create a DesmondStageFrame instance :type master: `MultiStageArea` :param master: The panel widget :type layout: QLayout :param layout: The layout the frame should be placed into :type copy_stage: `StageFrame` :param copy_stage: The StageFrame this StageFrame should be a copy of. The default is None, which will create a new default stage. :param stage_type: The type of stage to create, should be something meaningful to the subclass. The value is stored but not used in this parent class. :type icons: set :param icons: A set of module constants indicating which icons should be made into control buttons in the upper right corner. UP, DOWN, OPEN, CLOSE, DELETE, COPY """ self.master = master swidgets.SFrame.__init__(self, layout_type=swidgets.VERTICAL) if layout is not None: # Insert this stage before the stretch at the bottom of the layout layout.insertWidget(layout.count() - 1, self) if icons is not None: self.icons_to_use = icons else: self.icons_to_use = {DOWN, UP, CLOSE, OPEN, DELETE, COPY} # Control bar across the top self.bar_layout = swidgets.SHBoxLayout(layout=self.mylayout) # Label button self.label_button = QtWidgets.QToolButton() self.label_button.setAutoRaise(True) self.label_button.clicked.connect(self.toggleVisibility) self.bar_layout.addWidget(self.label_button) self.bar_layout.addStretch() # Top right control buttons self.createControlButtons() self.stage_type = stage_type # Toggle frame - stuff in here gets shown/hidden when the stage gets # compacted/contracted self.toggle_frame = swidgets.SFrame( layout=self.mylayout, layout_type=swidgets.VERTICAL) self.layOut(copy_stage=copy_stage) self.initialize(copy_stage=copy_stage) # Bottom dividing line Divider(self.mylayout) self.updateLabel()
[docs] def layOut(self, copy_stage=None): """ Lay out any custom widgets :type copy_stage: `StageFrame` :param copy_stage: The StageFrame this StageFrame should be a copy of. The default is None, which will create a new default stage. """ layout = self.toggle_frame.mylayout
[docs] def initialize(self, copy_stage=None): """ Perform any custom initialization before the widget is finalized :type copy_stage: `StageFrame` :param copy_stage: The StageFrame this StageFrame should be a copy of. The default is None, which will create a new default stage. """
[docs] def createControlButtons(self): """ Create upper-right corner control buttons as requested by the user """ self.createIconsIfNecessary() if CLOSE in self.icons: self.toggle_button = StageControlButton( self.icons[CLOSE], self.bar_layout, self.toggleVisibility) else: self.toggle_button = None if UP in self.icons: up_button = StageControlButton(self.icons[UP], self.bar_layout, self.moveUp) if DOWN in self.icons: down_button = StageControlButton(self.icons[DOWN], self.bar_layout, self.moveDown) if COPY in self.icons: copy_button = StageControlButton(self.icons[COPY], self.bar_layout, self.copy) if DELETE in self.icons: delete_button = StageControlButton(self.icons[DELETE], self.bar_layout, self.delete)
[docs] def createIconsIfNecessary(self): """ Factory to create button icons if they have not been created """ prefix = ":/schrodinger/application/matsci/msicons/" if not self.icons: if DOWN in self.icons_to_use: self.icons[DOWN] = QtGui.QIcon(prefix + "down.png") if UP in self.icons_to_use: self.icons[UP] = QtGui.QIcon(prefix + "up.png") if CLOSE in self.icons_to_use: self.icons[OPEN] = QtGui.QIcon(prefix + "plus.png") if OPEN in self.icons_to_use: self.icons[CLOSE] = QtGui.QIcon(prefix + "minus.png") if DELETE in self.icons_to_use: self.icons[DELETE] = QtGui.QIcon(prefix + "ex.png") if COPY in self.icons_to_use: self.icons[COPY] = QtGui.QIcon(prefix + "copy.png")
[docs] def toggleVisibility(self, checked=None, show=None): """ Show or hide the stage :type checked: bool :param checked: Not used, but swallows the PyQt clicked signal argument so that show doesn't get overwritten :type show: bool :param show: If True """ show_frame = show or (self.toggle_frame.isHidden() and show is None) if show_frame: self.toggle_frame.show() if self.toggle_button and CLOSE in self.icons: self.toggle_button.setIcon(self.icons[CLOSE]) else: self.toggle_frame.hide() if self.toggle_button and OPEN in self.icons: self.toggle_button.setIcon(self.icons[OPEN]) self.updateLabel()
[docs] def updateLabel(self): """ Set the label of the title button that toggles the stage open and closed """ # Set the user-facing index to be 1-indexed rather than 0-indexed index = self.master.getStageIndex(self) + 1 self.label_button.setText('(%d)' % index)
[docs] def moveUp(self): """ Move the stage up towards the top of the panel 1 stage """ self.master.moveStageUp(self)
[docs] def moveDown(self): """ Move the stage down towards the bottom of the panel 1 stage """ self.master.moveStageDown(self)
[docs] def delete(self): """ Delete this stage """ self.master.deleteStage(self)
[docs] def copy(self): """ Create a copy of this stage """ self.master.copyStage(self)
[docs] def reset(self): """ Resets the parameters to their default values. """ self.toggleVisibility(show=True)
[docs]class MultiStageArea(QtWidgets.QScrollArea): """ A scrollable frame meant to hold multiple stages. See the MatSci Desmond Multistage Simulation Workflow as one example. """
[docs] def __init__(self, layout=None, append_button=True, append_stretch=True, stage_class=StageFrame, control_all_buttons=False): """ Create a MultiStageArea instance :type layout: QBoxLayout :param layout: The layout to place this Area into :type append_button: bool :param append_button: Whether to add an "Append Stage" button to a Horizontal layout below the scrolling area :type append_stretch: bool :param append_button: Whether to add a QSpacer to the layout containing the append button. Use False if additional widgets will be added after creating the area. :type stage_class: `StageFrame` :param stage_class: The class used to create new stages :param bool control_all_buttons: True if buttons to control the open/closed state of all stages should be added above the stage area, False if not. Note that if layout is not supplied, the top_control_layout will have to be added to a layout manually. """ self.top_control_layout = swidgets.SHBoxLayout(layout=layout) if control_all_buttons: self.top_control_layout.addStretch() # Expand/Collapse all for slot, text, tip in [(self.collapseAll, '--', 'Collapse all stages'), (self.expandAll, '++', 'Expand all stages')]: btn = swidgets.SToolButton( text=text, layout=self.top_control_layout, tip=tip, command=slot) # Set up the scrolling area QtWidgets.QScrollArea.__init__(self) if layout is not None: layout.addWidget(self) self.setWidgetResizable(True) self.frame = swidgets.SFrame(layout_type=swidgets.VERTICAL) self.setWidget(self.frame) self.stage_layout = self.frame.mylayout # Add the append button if requested if append_button: self.button_layout = swidgets.SHBoxLayout(layout=layout) self.append_btn = swidgets.SPushButton( 'Append Stage', layout=self.button_layout, command=self.addStage) if append_stretch: self.button_layout.addStretch() # Add the first stage self.stages = [] self.stage_class = stage_class # Using a stretch factor of 100 allows this stretch to "overpower" the # vertical stretches in any stages. This means that stages can use # stretches to pack themselves tightly without expanding the stage to # greater than its necessary size. self.stage_layout.addStretch(100)
[docs] def addStage(self, copy_stage=None, stage_type=None, **kwargs): """ Add a new stage :type copy_stage: `StageFrame` :param copy_stage: The stage to copy. The default is None, which will create a new default stage. :param stage_type: What type of stage to add. Must be dealt with in the StageFrame subclass :rtype: `StageFrame` :return: The newly created stage :note: All other keyword arguments are passed to the stage class """ stage = self.stage_class( self, self.stage_layout, copy_stage=copy_stage, stage_type=stage_type, **kwargs) self.stages.append(stage) # The duplicate processEvents lines are not a mistake. For whatever # reason, both are required in order for the stage area to properly # compute its maximum slider value. QtWidgets.QApplication.instance().processEvents() QtWidgets.QApplication.instance().processEvents() sbar = self.verticalScrollBar() sbar.triggerAction(sbar.SliderToMaximum) return stage
[docs] def getStageIndex(self, stage): """ Return which stage number this is :type stage: StageFrame :param stage: Returns the index for this stage in the stage list :rtype: int :return: The stage number (starting at 0) """ try: return self.stages.index(stage) except ValueError: # Stages don't exist while they are being made return len(self.stages)
[docs] def moveStageUp(self, stage): """ Shift the given stage up one stage :type stage: StageFrame :param stage: The stage to move up """ current = self.getStageIndex(stage) if not current: return new = current - 1 self.moveStage(current, new)
[docs] def moveStageDown(self, stage): """ Shift the given stage down one stage :type stage: StageFrame :param stage: The stage to move down """ current = self.getStageIndex(stage) if current == len(self.stages) - 1: return new = current + 1 self.moveStage(current, new)
[docs] def moveStage(self, current, new): """ Move the a stage :type current: int :param current: The current position of the stage :type new: int :param new: The desired new position of the stage """ stage = self.stages.pop(current) self.stages.insert(new, stage) self.stage_layout.takeAt(current) self.stage_layout.insertWidget(new, stage) self.updateStageLabels(start_at=min(current, new))
[docs] def copyStage(self, stage, **kwargs): """ Create a copy of stage and add it directly below stage :type stage: StageFrame :param stage: The stage to copy :note: All keyword arguments are passed to addStage """ # Create it self.addStage(copy_stage=stage, **kwargs) # Move it to directly below the old stage new_index = self.getStageIndex(stage) + 1 current_index = len(self.stages) - 1 if new_index != current_index: self.moveStage(current_index, new_index)
[docs] def deleteStage(self, stage, update=True): """ Delete a stage :type stage: `StageFrame` :param stage: The stage to be deleted :type update: bool :param update: True if stage labels should be updated, False if not (use False if all stages are being deleted) """ index = self.getStageIndex(stage) self.stages.remove(stage) stage.setAttribute(Qt.WA_DeleteOnClose) stage.close() if update: self.updateStageLabels(start_at=index)
[docs] def updateStageLabels(self, start_at=0): """ Update stage labels - usually due to a change in stage numbering :type start_at: int :param start_at: All stages from this stage to the end of the stage list will be updated """ for stage in self.stages[start_at:]: stage.updateLabel()
[docs] def reset(self): """ Reset the stage area """ for stage in self.stages[:]: self.deleteStage(stage, update=False) self.addStage()
[docs] def expandAll(self): """ Expand all stages """ for stage in self.stages: stage.toggleVisibility(show=True)
[docs] def collapseAll(self): """ Collapse all stages """ for stage in self.stages: stage.toggleVisibility(show=False)
[docs]class StageControlButton(QtWidgets.QToolButton): """ The QToolButtons on the right of each `StageFrame` """
[docs] def __init__(self, icon, layout, command): """ Create a StageControlButton instance :type icon: QIcon :param icon: The icon for the button :type layout: QLayout :param layout: The layout the button should be placed in :type command: callable :param command: The slot for the clicked() signal """ QtWidgets.QToolButton.__init__(self) self.setIcon(icon) layout.addWidget(self) self.clicked.connect(command) self.setIconSize(QtCore.QSize(11, 11)) self.setAutoRaise(True)
[docs]class Divider(QtWidgets.QFrame): """ A raised divider line """
[docs] def __init__(self, layout): """ Create a Divider instance :type layout: QLayout :param layout: The layout the Divider should be added to """ QtWidgets.QFrame.__init__(self) self.setFrameShape(self.HLine) self.setFrameShadow(self.Raised) self.setLineWidth(5) self.setMinimumHeight(5) layout.addWidget(self)
[docs]class SaveDesmondFilesWidget(swidgets.SFrame): """ Widget that provides options for saving intermediate Desmond job files. """
[docs] def __init__(self, combined_trj=False, layout=None, **kwargs): """ Create an instance. :param bool combined_trj: Whether the checkbox for combining trajectory should be added. :param: layout: Layout to add this widget to :type layout: QtWidgets.QLayout """ super().__init__(layout=layout) self.combined_trj = combined_trj save_label = 'Save intermediate data: ' self.save_combo = swidgets.SComboBox(itemdict=SAVE_COMBO_OPTIONS) self.save_cbw = swidgets.SCheckBoxWithSubWidget( save_label, self.save_combo, layout=self.mylayout, **kwargs) if combined_trj: self.combine_cb = swidgets.SCheckBoxToggle( 'Combined trajectory', layout=self.mylayout, disabled_checkstate=False, checked=True)
[docs] def getCommandLineArgs(self): """ Return the command line flags to be used based on the widget state. Note that this expects driver classes to the `jobutils.SAVE_FLAG` with the allowed options found in l{jobutils.SAVE_FLAG_OPTS}. :return: List of command line args to use :rtype: list """ args = [jobutils.SAVE_FLAG] if not self.save_cbw.isChecked(): args.append(jobutils.SAVE_NONE) else: args.append(self.save_combo.currentData()) if self.combined_trj and self.combine_cb.isChecked(): args.append(jobutils.FLAG_COMBINE_TRJ) return args
[docs] def setFromCommandLineFlags(self, flags): """ Set the state of these widgets from command line flag values :param dict flags: Keys are command line flags, values are flag values. For flags that take no value, the value is ignored - the presence of the key indicates the flag is present. """ value = flags.get(jobutils.SAVE_FLAG) self.save_cbw.setChecked(value is not None) if value is not None: self.save_combo.setCurrentData(value) if self.combined_trj: self.combine_cb.setChecked(jobutils.FLAG_COMBINE_TRJ in flags)
[docs] def reset(self): """ Reset the frame """ self.save_cbw.reset() if self.combined_trj: self.combine_cb.reset()
[docs]class RandomSeedWidget(swidgets.SCheckBoxWithSubWidget): """ Standardized checkbox with spinbox to provide option to specify random seed. The spinbox is hidden when the checkbox is not checked. """
[docs] def __init__(self, layout=None, minimum=parserutils.RANDOM_SEED_MIN, maximum=parserutils.RANDOM_SEED_MAX, default=parserutils.RANDOM_SEED_DEFAULT, **kwargs): """ :param `QtWidgets.QLayout` layout: Layout to add this widget to :param int minimum: The minimum acceptable random seed value :param int maximum: The maximum acceptable random seed value :param int default: The default custom random seed value """ self.seed_sb = swidgets.SSpinBox( minimum=minimum, maximum=maximum, value=default, stepsize=1) tip = kwargs.get('tip') if tip: self.seed_sb.setToolTip(tip) if kwargs.get('checked') is None: kwargs['checked'] = True super(RandomSeedWidget, self).__init__( 'Set random number seed', self.seed_sb, layout=layout, **kwargs)
[docs] def getSeed(self): """ Return the value specified in the spinbox. :return: seed for random number generator :rtype: int """ return self.seed_sb.value()
[docs] def getCommandLineFlag(self): """ Return a list containing the proper random seed flag and argument specified by the state of this widget. Meant to be added onto a command list, e.g. cmd = [EXEC, infile_path] cmd += rs_widget.getCommandLineFlag() :return: A list containing the random seed flag followed by either "random", or the string representation of the seed specified in this widget's spinbox. :rtype: `list` of two `str` """ if not self.isChecked(): seed = parserutils.RANDOM_SEED_RANDOM else: seed = str(self.getSeed()) return [jobutils.FLAG_RANDOM_SEED, seed]
[docs] def setFromCommandLineFlags(self, flags): """ Set the state of these widgets from command line flag values :param dict flags: Keys are command line flags, values are flag values. For flags that take no value, the value is ignored - the presence of the key indicates the flag is present. """ seed = flags.get(jobutils.FLAG_RANDOM_SEED) if seed is None or seed == parserutils.RANDOM_SEED_RANDOM: self.setChecked(False) else: self.setChecked(True) self.seed_sb.setValue(int(seed)) self._toggleStateChanged(self.isChecked())
def _toggleStateChanged(self, state): """ React to the checkbox changing checked state Overwrite the parent class to hide the subwidget instead of disabling it :type state: bool :param state: The new checked state of the checkbox """ if self.reverse_state: self.subwidget.setVisible(not state) else: self.subwidget.setVisible(state)
[docs] def setVisible(self, state): """ Overwrite the parent method to allow the checkbox state to control the visibility of the subwidget """ super().setVisible(state) if state: self._toggleStateChanged(self.isChecked()) else: self.subwidget.setVisible(False)
[docs]class DefineASLDialog(swidgets.SDialog): """ Manage defining an ASL. """
[docs] def __init__(self, master, help_topic='', show_markers=False, struct=None): """ Create an instance. :type master: QtWidgets.QWidget :param master: the window to which this dialog should be WindowModal :type help_topic: str :param help_topic: an optional help topic :type show_markers: bool :param show_markers: whether to show the Markers checkbox :type struct: schrodinger.structure.Structure :param struct: an optional structure against which the ASL will be validated """ self.show_markers = show_markers self.struct = struct self.indices = None dbb = QtWidgets.QDialogButtonBox buttons = [dbb.Ok, dbb.Cancel, dbb.Reset] if help_topic: buttons.append(dbb.Help) title = 'Define ASL' swidgets.SDialog.__init__( self, master, standard_buttons=buttons, help_topic=help_topic, title=title) self.setWindowModality(QtCore.Qt.WindowModal)
[docs] def layOut(self): """ Lay out the widgets. """ self.atom_selector = atomselector.AtomSelector( self, label='ASL', show_markers=self.show_markers) self.mylayout.addWidget(self.atom_selector) self.mylayout.addStretch()
[docs] def getAsl(self): """ Return the ASL. :rtype: str :return: the ASL """ return self.atom_selector.getAsl().strip()
[docs] def getIndices(self): """ If a structure was provided at instantiation then return the atom indices of the provided structure that match the specified ASL. :rtype: list :return: matching atom indices or None if no structure was provided """ if self.struct and self.indices is None: asl = self.getAsl() self.indices = analyze.evaluate_asl(self.struct, asl) return self.indices
[docs] def isValid(self): """ Return True if valid, (False, msg) otherwise. :rtype: bool, pair tuple :return: True if valid, (False, msg) otherwise """ asl = self.getAsl() if not analyze.validate_asl(asl): msg = ('The specified ASL is invalid.') return (False, msg) elif self.struct and not self.getIndices(): msg = ('The specified ASL does not match the given structure.') return (False, msg) return True
def _stopPicking(self): """ Stop picking. """ self.atom_selector.picksite_wrapper.stop() def _hideMarkers(self): """ Hide markers. """ if self.show_markers: self.atom_selector.marker.hide() self.atom_selector.marker.setAsl('not all')
[docs] def accept(self): """ Callback for the Accept (OK) button. """ state = self.isValid() if state is not True: self.error(state[1]) return self._stopPicking() self._hideMarkers() return swidgets.SDialog.accept(self)
[docs] def reject(self): """ Callback for the Reject (Cancel) button. """ self._stopPicking() self._hideMarkers() return swidgets.SDialog.reject(self)
[docs] def reset(self): """ Reset it. """ self.atom_selector.reset() self._hideMarkers() self.indices = None
[docs]class SideHistogram(object): """ Class to setup a side histogram plot in the passed figure. """
[docs] def __init__(self, figure, x_label, y_label='Frequency', x_start=0.6, x_end=0.9, y_start=0.2, y_end=0.9, color='c', face_color='white', fontsize='small', title=None, subplot_pos=None, flip_ylabel=False): """ Setup an additional axes on the right half of the canvas. :param matplotlib.figure.Figure figure: add histogram plot to this figure :param str x_label: name for the x-axis label :param str y_label: name for the y-axis label :param float x_start: relative x-coordinate on the figure where the plot should start from :param float x_end: relative x-coordinate on the figure where the plot should end :param float y_start: relative y-coordinate on the figure where the plot should start from :param float y_end: relative x-coordinate on the figure where the plot should end :param str color: color of the bar of the histogram :param str face_color: bg color of the plot :param int fontsize: font size for the lables :param str title: title for the plot :param int subplot_pos: A three digit integer, where the first digit is the number of rows, the second the number of columns, and the third the index of the current subplot. Index goes left to right, followed but top to bottom. Hence in a 4 grid, top-left is 1, top-right is 2 bottom left is 3 and bottom right is 4. Subplot overrides x_start, y_start, x_end, and y_end. :param bool flip_ylabel: If True will move tics and labels of y-axis to right instead of left. In case of False, it won't """ self.hist_collections = None self.hist_data = [[], []] self.fontsize = fontsize self.color = color self.figure = figure self.subplot_pos = subplot_pos if self.subplot_pos: self.plot = self.figure.add_subplot(subplot_pos) else: self.plot = self.figure.add_axes( [x_start, y_start, x_end - x_start, y_end - y_start]) self.plot.set_facecolor('white') if title is not None: self.plot.set_title(title, size=self.fontsize) self.plot.set_ylabel(y_label, size=self.fontsize) self.plot.set_xlabel(x_label, size=self.fontsize) self.plot.tick_params(labelsize=self.fontsize) if flip_ylabel: self.plot.yaxis.tick_right() self.plot.yaxis.set_label_position("right") # Save default x/y limit, tick, and label self.d_xticks = self.plot.get_xticks() self.d_xticks = list(map(lambda x: round(x, 2), self.d_xticks)) self.default_xlim = self.plot.get_xlim() self.d_yticks = self.plot.get_yticks() self.d_yticks = list(map(lambda x: round(x, 2), self.d_yticks)) self.default_ylim = self.plot.get_ylim() self.reset()
[docs] def replot(self, data, bins=10): """ Remove previous histogram (if exists), plot a new one, and force the canvas to draw. :param list data: list of data to plot histogram :param int bins: number of bins for the histogram """ if self.hist_collections: for bar in self.hist_collections: bar.remove() hist_counts, bin_edges, self.hist_collections = self.plot.hist( data, color=self.color, bins=bins) self.hist_data = [[ (a + b) / 2 for a, b in zip(bin_edges[:-1], bin_edges[1:]) ], hist_counts] half_bin_width = (bin_edges[1] - bin_edges[0]) / 2. self.plot.set_xlim(bin_edges[0] - half_bin_width, bin_edges[-1] + half_bin_width) # 5% blank each side; 5 xticks intvl = round(len(self.hist_data[0]) / 5.) or 1 xtick_values = [x for x in self.hist_data[0][0::intvl]] self.plot.set_xticks(xtick_values) self.plot.set_xticklabels([round(x, 2) for x in xtick_values]) # 10% blank on top; 5 - 8 yticks ylim_max = max(hist_counts) * 1.1 ytick_intvl = max([int(float('%.1g' % (ylim_max * 2. / 5.)) / 2.), 1]) ytick_values = [ ytick_intvl * idx for idx in range(math.ceil(ylim_max / ytick_intvl)) ] self.plot.set_ylim(0, ylim_max) self.plot.set_yticks(ytick_values) self.plot.set_yticklabels(ytick_values) # Prevent ylabel overlapping in case of multiple plots if self.subplot_pos: self.figure.tight_layout()
[docs] def reset(self): """ Reset Histogram plot. """ if self.hist_collections is None: return for bar in self.hist_collections: bar.remove() self.hist_collections = None # Reset x/y limit, tick, and label self.plot.set_xlim(self.default_xlim) self.plot.set_xticks(self.d_xticks) self.plot.set_xticklabels(self.d_xticks) self.plot.set_ylim(self.default_ylim) self.plot.set_yticks(self.d_yticks) self.plot.set_yticklabels(self.d_yticks) self.hist_data = [[], []] self.figure.tight_layout()
[docs]class SliderchartVLPlot(sliderchart.SliderPlot): """ Overide the SliderPlot class in sliderchart to provide vertical slide bars and significant figure round. """
[docs] def __init__(self, **kwargs): """ See the parent class for documentation """ super().__init__(use_hsliders=False, **kwargs)
[docs] def setVsliderPosition(self, slider_id, value): """ Set the position of vertical sliders. :type slider_id: int :param slider_id: 0 means the left slider; 1 means the right one :type value: float :param value: The new x value to attempt to place the slider at :rtype: float :return: final slider position, corrected by x range and the other slider """ if not (self.x_range[0] < value < self.x_range[1]): value = self.x_range[slider_id] self.vsliders[slider_id].setPosition(value) value = self.vsliders[slider_id].getPosition() return value
[docs] def updateSlider(self, slider_idx, fit_edit=None, value=None, draw=True): """ Change the slider to the user typed position, read this new position, and set the widget to this new position. At least one of value and fit_edit must be provided, and only read fit_edit when value is not provided. :param fit_edit: swidgets.EditWithFocusOutEvent or None :type fit_edit: The text of this widget defines one fitting boundary, and the text may be changed according to the newly adjusted boundary. :param slider_idx: int (0 or 1) :type slider_idx: 0 --> left vertical slider; 1 --> right vertical slider; :param value: float :type value: set slider to value position, if not None :param draw: bool :type draw: force the canvas to draw :rtype: float :return: left or right slider position """ if value is None and fit_edit is not None: try: value = float(fit_edit.text()) except ValueError: return value = self.setVsliderPosition(slider_idx, value) if fit_edit: fit_edit.setText(msutils.sig_fig_round(value)) if draw: self.canvas.draw() return value
[docs] def getVSliderIndexes(self): """ Get the data indexes of the left and right vertical sliders. Requires that the x data is sorted ascending. :rtype: (int, int) or (None, None) :return: The data indexes of the left and right vertical sliders, or None, None if the sliders are outside the data range """ x_min = self.getVSliderMin() for idx, val in enumerate(self.original_xvals): if x_min <= val: x_min_idx = idx break else: return None, None x_max = self.getVSliderMax() for idx, val in enumerate(reversed(self.original_xvals), start=1): if x_max >= val: x_max_idx = len(self.original_xvals) - idx break else: return None, None return x_min_idx, x_max_idx
[docs] def removeSliders(self, draw=True): """ Remove vertical and horizontal sliders from the chart :param bool draw: Whether canvas should be redrawn """ for sliders in (self.vsliders, self.hsliders): for _ in range(len(sliders)): sliders.pop(0).remove() if draw: self.canvas.draw()
[docs]class SliderchartVLFitStdPlot(SliderchartVLPlot): """ Inherits the SliderchartVLPlot class. Provides line fitting and std plotting. """
[docs] def __init__(self, legend_loc='upper right', fit_linestyle='dashed', fit_linecolor='red', fit_linewidth=2., data_label='Data', std_label='STD', fit_label='Fitting', layout=None, **kwargs): """ See the parent class for documentation :type legend_loc: str :param legend_loc: the location of the legend :type fit_linestyle: str :param fit_linestyle: style of the fitted line :type fit_linecolor: str :param fit_linecolor: color of the fitted line :type fit_linewidth: float :param fit_linewidth: linewidth of the fitted line :type data_label: str :param data_label: legend label for data :type std_label: str :param std_label: legend label for standard deviation :type fit_label: float :param fit_label: legend label for fitting line :type layout: QLayout :keyword layout: layout to place the SliderchartVLFitStdPlot in """ self.legend_loc = legend_loc self.fit_linestyle = fit_linestyle self.fit_linecolor = fit_linecolor self.fit_linewidth = fit_linewidth self.data_label = data_label self.std_label = std_label self.fit_label = fit_label self.original_ystd = None self.variation = None self.fitted_line = None self.poly_collections = None self.legend = None super().__init__(**kwargs) self.default_y_label = self.y_label self.default_x_label = self.x_label self.default_title = self.title if layout is not None: layout.addWidget(self)
[docs] def reset(self): """ Reset the labels, title, and plot. """ self.y_label = self.default_y_label self.x_label = self.default_x_label self.title = self.default_title self.setXYYStd([], [], replot=True)
[docs] def replot(self, fit_only=False, *args, **kwargs): """ See the parent class for documentation :type fit_only: bool :param fit_only: if True, only update the fitted line. :rtype: namedtuple :return: fitting parameters """ if self.fitted_line is not None: self.fitted_line.remove() self.fitted_line = None if not fit_only: if self.poly_collections is not None: self.poly_collections.remove() self.poly_collections = None super().replot() self.plotYSTd() data_fit = self.plotFitting(*args, **kwargs) self.updateLegend() sliderchart.prevent_overlapping_x_labels(self.canvas) return data_fit
[docs] def updateLegend(self): """ Update legend according to the plotted lines. """ legend_data, legend_txt = [], [] if self.original_ystd is not None: legend_data.append(self.poly_collections) legend_txt.append(self.std_label) if self.fitted_line is not None: legend_data.append(self.fitted_line) legend_txt.append(self.fit_label) if len(legend_txt): legend_data = [self.series] + legend_data legend_txt = [self.data_label] + legend_txt self.legend = self.plot.legend( legend_data, legend_txt, loc=self.legend_loc) elif self.legend: self.legend.remove() self.legend = None
[docs] def plotFitting(self): """ To be overwritten in child class. """ raise NotImplementedError( "`plotFitting' method not implemented by subclass")
[docs] def plotYSTd(self, color='green', alpha=0.5): """ Plot standard deviation as area. :type color: str :param color: area color :type alpha: float :param alpha: set area transparent """ if self.original_ystd is None: return ystd_num = len(self.original_ystd) x = self.original_xvals[:ystd_num] y = self.original_yvals[:ystd_num] y_plus_std = y + self.original_ystd y_minus_std = y - self.original_ystd self.poly_collections = self.plot.fill_between( x, y_minus_std, y_plus_std, color=color, alpha=alpha) self.plot.set_ylim((min(y_minus_std), max(y_plus_std)))
[docs] def variationChanged(self, variation_edit, upper_tau_edit, min_data_num=10): """ Response to the variation widget change. Move the upper slider so that the data between the two slider bars have coefficient of variation smaller than the variation widget value. :param variation_edit: swidgets.EditWithFocusOutEvent :type variation_edit: The text of this widget defines the coefficient of variation. :param upper_tau_edit: swidgets.EditWithFocusOutEvent :type upper_tau_edit: The text of this widget defines upper fitting boundary :param min_data_num: int :type min_data_num: The minimum number of data points in the fitting range :raise ValueError: not enough data available """ if self.original_ystd is None: return variance_input = variation_edit.float() variation_edit.clear() ystd_num = len(self.original_ystd) xvals = self.original_xvals[:ystd_num] idx_bounds = [] for vslider in self.vsliders: idx_bounds.append(numpy.abs(xvals - vslider.getPosition()).argmin()) availabel_data_point = idx_bounds[1] - idx_bounds[0] if availabel_data_point < min_data_num: raise ValueError( 'Only %s data points found, but a minium of %s is required.' % (availabel_data_point, min_data_num)) # varince_input is given in percentage, convert to decimals std_allow = variance_input / 100. - self.variation # Starting from the frame end, find the first frame within the variation # or set to the closest position near the left slider bar for idx in range(idx_bounds[1], idx_bounds[0] + min_data_num, -1): if std_allow[idx] > 0: break self.setVsliderPosition(1, xvals[idx]) sliderchart.prevent_overlapping_x_labels(self.canvas) right_slider_pos = self.vsliders[1].getPosition() upper_tau_edit.setText(msutils.sig_fig_round(right_slider_pos)) variation_edit.setText( msutils.sig_fig_round(self.variation[idx] * 100.))
[docs] def setXYYStd(self, xvals, yvals, ystd=None, x_range=None, y_range=None, replot=True): """ Set the X values, Y values, and Y standard deviation of the plot. :type xvals: list :param xvals: the x values to plot :type yvals: list :param yvals: y series to plot, should be the same length as xvals :type ystd: list or None :param ystd: the standard deviation of y series to plot :type x_range: tuple or None :param x_range: (min, max) values for the X-axis, default is to show all values :type y_range: tuple or None :param y_range: (min, max) values for the Y-axis, default is to show all values :type replot: bool :param replot: True of plot should be redrawn (default), False if not. False can be used if a subsequent setY is required. """ # Set self.original_ystd before plotYSTd() is called # setXY() calls replot(), and replot() calls plotYSTd() self.original_ystd = ystd super().setXY( xvals, yvals, x_range=x_range, y_range=y_range, replot=replot) if ystd is None: self.variation = None return ystd_num = len(self.original_ystd) smooth_mean = ndimage.filters.gaussian_filter( self.original_yvals[:ystd_num], sigma=3) smooth_std = ndimage.filters.gaussian_filter(ystd, sigma=10) # x/0. = Inf self.variation = numpy.divide( smooth_std, smooth_mean, out=numpy.full_like(smooth_mean, numpy.Inf), where=smooth_mean != 0)
[docs] def setVarianceEdit(self, variance_le): """ Set the variance text and state. :param variance_le: swidgets.EditWithFocusOutEvent :type variance_le: The text of this widget shows the coefficient of variation """ variance_le.clear() if self.variation is None: variance_le.setEnabled(False) return upper_tau = self.vsliders[1].getPosition() xval_idx = (numpy.abs(self.original_xvals - upper_tau)).argmin() # Convert into 'xx %' format try: value = self.variation[xval_idx] * 100. except IndexError: # Standard deviation is from block average and has less data points return variance_le.setEnabled(True) variance_le.setText(msutils.sig_fig_round(value))
[docs]class StructureLoader(swidgets.SFrame): """ A set of widgets that allow the user to load a structure. """ structure_changed = QtCore.pyqtSignal() WORKSPACE = 'Included entry' FILE = 'From file' BUTTON_TEXT = {WORKSPACE: 'Import', FILE: 'Browse...'} NOT_LOADED = 'Not loaded' NOT_LOADED_TIP = 'Structure is not yet loaded' DIALOG_ID = 'STRUCTURE_LOADER_IR'
[docs] def __init__(self, master, label, maestro, parent_layout): """ Create StructureLoader object. :type master: QWidget :param master: Master widget :type label: str :param label: Label for the SLabeledComboBox widget :type maestro: `schrodinger.maestro.maestro` :param maestro: Maestro instance :type parent_layout: QLayout :param parent_layout: Parent layout """ self.master = master self.maestro = maestro self.struct = None swidgets.SFrame.__init__( self, layout=parent_layout, layout_type=swidgets.HORIZONTAL) layout = self.mylayout load_options = [self.WORKSPACE, self.FILE] if self.maestro else [self.FILE] self.combo = swidgets.SLabeledComboBox( label, items=load_options, layout=layout, stretch=False, command=self.typeChanged, nocall=True) self.button = swidgets.SPushButton( 'Import', command=self.loadStructure, layout=layout) self.label = swidgets.SLabel(self.NOT_LOADED, layout=layout) self.label.setToolTip(self.NOT_LOADED_TIP) layout.addStretch() self.reset()
[docs] def typeChanged(self): """ React to a change in the type of scaffold """ self.button.setText(self.BUTTON_TEXT[self.combo.currentText()])
[docs] def reset(self): """ Reset the widgets """ self.struct = None self.combo.reset() self.typeChanged() self.updateLabel()
[docs] def updateLabel(self): """ Update the status label. """ if self.struct: text = self.struct.title tip = text if len(text) > 25: text = text[:22] + '...' else: tip = self.NOT_LOADED_TIP text = self.NOT_LOADED self.label.setText(text) self.label.setToolTip(tip)
[docs] def loadStructure(self): """ Load a structure from the selected source. """ self.struct = None if self.combo.currentText() == self.WORKSPACE: loaded = self.importFromWorkspace() else: loaded = self.importFromFile() if loaded and self.struct and len(self.struct.atom) == 0: self.struct = None self.master.warning('The structure is empty, containing no atoms.') elif loaded: with qtutils.wait_cursor: self.updateLabel() self.structure_changed.emit()
[docs] def importFromWorkspace(self): """ Import a structure from the workspace. :rtype: bool or None :return: True if a structure was loaded successfully, None if not """ try: self.struct = self.maestro.get_included_entry() return True except RuntimeError: self.master.warning('There must be one and only one entry included' ' in the Workspace.')
[docs] def importFromFile(self): """ Import a structure from a file, including opening the dialog to allow the user to select the file. :rtype: bool or None :return: True if the structure was loaded successfully, None if not """ ffilter = 'Maestro files (*.mae *.maegz *.mae.gz *cms *cms.gz)' path = filedialog.get_open_file_name( parent=self.master, caption='Load Structure', filter=ffilter, id=self.DIALOG_ID) if not path: return try: self.struct = structure.Structure.read(path) # To distinguish whenever the structure was loaded from PT or file self.struct.property.pop('s_m_entry_id', None) return True except (IOError, mmcheck.MmException): self.master.warning( 'Unable to read structure information from %s' % path)
[docs] def getMolecularWeight(self): """ Get the molecular weight of the structure. :return: the molecular weight of the structure :rtype: float """ if self.struct is None: return 0. return self.struct.total_weight
[docs]class DesmondMDWidgets(swidgets.SFrame): """ Frame that holds core MD related fields, to be reused in the panels that submit desmond jobs. """ LE_WIDTH = 80 TRJ_NFRM_LABEL = 'yields %d frames' ENEGRP_NFRM_LABEL = 'yields %d records' ENESEQ_NFRM_LABEL = 'yields %d records' BOTTOM_DATOR = 1e-10 DEFAULTS = { jobutils.FLAG_MD_TIME: 1.0, jobutils.FLAG_MD_TIMESTEP: 2.0, jobutils.FLAG_MD_TRJ_INT: 4.8, jobutils.FLAG_MD_ENEGRP_INT: 4.8, jobutils.FLAG_MD_ENESEQ_INT: 1.2, jobutils.FLAG_MD_TEMP: 300.0, jobutils.FLAG_MD_PRESS: 1.01325 }
[docs] def __init__(self, time_changed_command=None, timestep_changed_command=None, show_temp=True, temp_changed_command=None, show_press=True, show_save=True, show_enegrp=False, show_eneseq=False, show_seed=True, enegrp_changed_command=None, ensembles=None, isotropy=None, defaults=None, time_use_ps=False, combined_trj=False, **kwargs): """ Initialize object and place widgets on the layout. See swidgets.SFrame for more documentation. :type time_changed_command: Method or None :param time_changed_command: Called on focus out event of MD time field :type timestep_changed_command: Method or None :param timestep_changed_command: Called on focus out event of MD time step field :type show_temp: bool :param show_temp: Show or not MD temperature field :type temp_changed_command: Method or None :param temp_changed_command: Called on focus out event of MD temp step field :type show_press: bool :param show_press: Show or not MD pressure field :type show_save: bool :param show_save: Show or not Save MD related data widget :type show_enegrp: bool :param show_enegrp: Show or not energy group recording interval widget :type show_eneseq: bool :param show_eneseq: Show or not energy recording interval widget :type show_seed: bool :param show_seed: Show or not random seed widget :type enegrp_changed_command: Method or None :param enegrp_changed_command: Called on focus out event of enegrp interval field :type ensembles: None or list :param ensembles: Show choice of desmond ensembles :type isotropy: None or dict :param isotropy: Show choice of desmond barostat isotropy policies :type defaults: dict or None :param defaults: Dict with the default values of MD fields :type time_use_ps: bool :param time_use_ps: If True, use ps for the time field, otherwise ns (which is default in Desmond GUI) """ def get_dator(): return swidgets.SNonNegativeRealValidator(bottom=self.BOTTOM_DATOR) self.time_use_ps = time_use_ps if self.time_use_ps: time_after_label = 'ps' else: time_after_label = 'ns' self.show_temp = show_temp self.temp_changed_command = temp_changed_command self.show_press = show_press self.show_save = show_save self.show_enegrp = show_enegrp self.show_eneseq = show_eneseq self.show_seed = show_seed self.enegrp_changed_command = enegrp_changed_command self.ensembles = ensembles self.time_changed_command = time_changed_command self.timestep_changed_command = timestep_changed_command self.isotropy = isotropy self.defaults = dict(self.DEFAULTS) if defaults: self.defaults.update(defaults) swidgets.SFrame.__init__(self, **kwargs) if self.ensembles: # Ensure that all ensembles provided are known by desmond assert len( set(self.ensembles).difference(set( desmondutils.ENSEMBLES))) == 0 self.ensemble_cb = swidgets.SLabeledComboBox( 'Ensemble class:', items=self.ensembles, layout=self.mylayout, command=self.onEnsembleChanged, nocall=True) hlayout = swidgets.SHBoxLayout(layout=self.mylayout) self.time_le = swidgets.EditWithFocusOutEvent( 'Simulation time:', edit_text=str(self.defaults[jobutils.FLAG_MD_TIME]), after_label=time_after_label, width=self.LE_WIDTH, validator=get_dator(), always_valid=True, focus_out_command=self.onTimeChanged, layout=hlayout, tip='Time for the Molecular Dynamics simulations', stretch=False) self.timestep_le = swidgets.EditWithFocusOutEvent( 'Time step:', edit_text=str(self.defaults[jobutils.FLAG_MD_TIMESTEP]), after_label='fs', width=self.LE_WIDTH, validator=get_dator(), always_valid=True, focus_out_command=self.onTimestepChanged, layout=hlayout, tip='Time step for the Molecular Dynamics simulations') hlayout.addStretch(1000) hlayout_trj = swidgets.SHBoxLayout(layout=self.mylayout) self.trj_int_le = swidgets.EditWithFocusOutEvent( 'Trajectory recording interval:', edit_text=str(self.defaults[jobutils.FLAG_MD_TRJ_INT]), after_label='ps', width=self.LE_WIDTH, validator=get_dator(), always_valid=True, focus_out_command=self.onTrjIntervalChanged, layout=hlayout_trj, tip='Trajectory recording interval', stretch=False) self.trj_nfrm_le = swidgets.SLabel( self.TRJ_NFRM_LABEL % 10, layout=hlayout_trj) hlayout_trj.addStretch(1000) if self.show_enegrp: hlayout_enegrp = swidgets.SHBoxLayout(layout=self.mylayout) self.enegrp_int_le = swidgets.EditWithFocusOutEvent( 'Energy group recording interval:', edit_text=str(self.defaults[jobutils.FLAG_MD_ENEGRP_INT]), after_label='ps', width=self.LE_WIDTH, validator=get_dator(), always_valid=True, focus_out_command=self.onEneGrpIntervalChanged, layout=hlayout_enegrp, tip='Energy group recording interval', stretch=False) self.enegrp_nfrm_le = swidgets.SLabel( self.ENEGRP_NFRM_LABEL % 10, layout=hlayout_enegrp) hlayout_enegrp.addStretch(1000) if self.show_eneseq: hlayout_eneseq = swidgets.SHBoxLayout(layout=self.mylayout) self.eneseq_int_le = swidgets.EditWithFocusOutEvent( 'Energy recording interval:', edit_text=str(self.defaults[jobutils.FLAG_MD_ENESEQ_INT]), after_label='ps', width=self.LE_WIDTH, validator=get_dator(), always_valid=True, focus_out_command=self.onEneSeqIntervalChanged, layout=hlayout_eneseq, tip='Energy recording interval', stretch=False) self.eneseq_nfrm_le = swidgets.SLabel( self.ENESEQ_NFRM_LABEL % 10, layout=hlayout_eneseq) hlayout_eneseq.addStretch(1000) hlayout_tp = swidgets.SHBoxLayout(layout=self.mylayout) self.temp_le = swidgets.EditWithFocusOutEvent( 'Temperature:', edit_text=str(self.defaults[jobutils.FLAG_MD_TEMP]), after_label='K', width=self.LE_WIDTH, validator=get_dator(), always_valid=True, focus_out_command=self.temp_changed_command, layout=hlayout_tp, tip='Temperature for the Molecular Dynamics simulations', stretch=False) self.temp_le.setVisible(self.show_temp) self.press_le = swidgets.SLabeledEdit( 'Pressure:', edit_text=str(self.defaults[jobutils.FLAG_MD_PRESS]), after_label='bar', width=self.LE_WIDTH, validator=get_dator(), always_valid=True, layout=hlayout_tp, tip='Pressure for the Molecular Dynamics simulations') if self.isotropy: # Ensure that all barostat isotropy type provided are known by desmond assert set(self.isotropy.values()).issubset( set(dconst.IsotropyPolicy)) self.isotropy_cb = swidgets.SLabeledComboBox( 'Barostat isotropy:', itemdict=self.isotropy, layout=hlayout_tp, tip='Barostat isotropy type') hlayout_tp.addStretch(1000) self.press_le.setVisible(self.show_press) if self.show_seed: self.random_seed = RandomSeedWidget(layout=self.mylayout) self.save_widget = SaveDesmondFilesWidget( combined_trj=combined_trj, layout=self.mylayout) self.save_widget.setVisible(self.show_save) self.reset()
[docs] def updateTrjFrames(self): """ Update approximate number of trajectory frames and trajectory interval (if needed). """ time_ps = self.getTimePS() if self.trj_int_le.float() > time_ps: self.trj_int_le.setText(str(time_ps)) self.trj_nfrm_le.setText( self.TRJ_NFRM_LABEL % math.floor(time_ps / self.trj_int_le.float()))
[docs] def updateEneGrpFrames(self): """ Update approximate the number of energy group recordings and interval (if needed). """ if not self.show_enegrp: return time_ps = self.getTimePS() if self.enegrp_int_le.float() > time_ps: self.enegrp_int_le.setText(str(time_ps)) self.enegrp_nfrm_le.setText(self.ENEGRP_NFRM_LABEL % math.floor( time_ps / self.enegrp_int_le.float()))
[docs] def updateEneSeqFrames(self): """ Update approximate the number of energy recordings and interval (if needed). """ if not self.show_eneseq: return time_ps = self.getTimePS() if self.eneseq_int_le.float() > time_ps: self.eneseq_int_le.setText(str(time_ps)) self.eneseq_nfrm_le.setText(self.ENESEQ_NFRM_LABEL % math.floor( time_ps / self.eneseq_int_le.float()))
[docs] def getTimePS(self): """ Returns the simulation time in picoseconds. :rtype: float :return: Time in picoseconds """ time_ps = self.time_le.float() if not self.time_use_ps: # Default is time in ns time_ps *= units.NANO2PICO return time_ps
[docs] def onTimeChanged(self): """ Called when simulation time changes. """ self.updateTrjFrames() self.updateEneGrpFrames() self.updateEneSeqFrames() if self.time_changed_command: self.time_changed_command()
[docs] def onTimestepChanged(self): """ Called when time step changes. """ if self.timestep_changed_command: self.timestep_changed_command()
[docs] def onTrjIntervalChanged(self): """ Called when trajectory interval changes. """ self.updateTrjFrames()
[docs] def onEneGrpIntervalChanged(self): """ Called when energy group interval changes. """ self.updateEneGrpFrames() if self.enegrp_changed_command: self.enegrp_changed_command()
[docs] def onEneSeqIntervalChanged(self): """ Called when energy interval changes. """ self.updateEneSeqFrames()
[docs] def onEnsembleChanged(self): """ Called when ensemble changes. """ if not self.show_press or not self.ensembles: return # Disable pressure field if ensemble doesn't have pressure in it is_p_ensemble = 'P' in self.ensemble_cb.currentText() self.press_le.setEnabled(is_p_ensemble) if self.isotropy: self.isotropy_cb.setEnabled(is_p_ensemble)
[docs] def getCommandLineFlags(self): """ Return a list containing the proper command-line flags and their values, e.g. cmd = [EXEC, infile_path] cmd += rs_widget.getCommandLineFlag() :rtype: list :return: command-line flags and their values """ ret = [ jobutils.FLAG_MD_TIME, self.time_le.text(), jobutils.FLAG_MD_TIMESTEP, self.timestep_le.text(), jobutils.FLAG_MD_TRJ_INT, self.trj_int_le.text() ] if self.show_temp: ret += [jobutils.FLAG_MD_TEMP, self.temp_le.text()] if self.show_press: ret += [jobutils.FLAG_MD_PRESS, self.press_le.text()] if self.show_enegrp: ret += [jobutils.FLAG_MD_ENEGRP_INT, self.enegrp_int_le.text()] if self.show_eneseq: ret += [jobutils.FLAG_MD_ENESEQ_INT, self.eneseq_int_le.text()] if self.ensembles: ret += [jobutils.FLAG_MD_ENSEMBLE, self.ensemble_cb.currentText()] if self.isotropy: ret += [jobutils.FLAG_MD_ISOTROPY, self.isotropy_cb.currentData()] if self.show_seed: ret += self.random_seed.getCommandLineFlag() if self.show_save: ret += self.save_widget.getCommandLineArgs() return ret
[docs] def setFromCommandLineFlags(self, flags): """ Set the state of these widgets from command line flag values :param dict flags: Keys are command line flags, values are flag values. For flags that take no value, the value is ignored - the presence of the key indicates the flag is present. """ flag_to_edit = { jobutils.FLAG_MD_TIME: self.time_le, jobutils.FLAG_MD_TIMESTEP: self.timestep_le, jobutils.FLAG_MD_TRJ_INT: self.trj_int_le, jobutils.FLAG_MD_TEMP: self.temp_le, jobutils.FLAG_MD_PRESS: self.press_le } if self.show_enegrp: flag_to_edit[jobutils.FLAG_MD_ENEGRP_INT] = self.enegrp_int_le if self.show_eneseq: flag_to_edit[jobutils.FLAG_MD_ENESEQ_INT] = self.eneseq_int_le for aflag, edit in flag_to_edit.items(): value = flags.get(aflag) if value is not None: edit.setText(str(value)) ensemble = flags.get(jobutils.FLAG_MD_ENSEMBLE) if ensemble: self.ensemble_cb.setCurrentText(ensemble) if self.isotropy: isotropy = flags.get(jobutils.FLAG_MD_ISOTROPY) if isotropy: self.isotropy_cb.setCurrentData(isotropy) if self.show_seed: self.random_seed.setFromCommandLineFlags(flags) self.save_widget.setFromCommandLineFlags(flags)
[docs] def reset(self): """ Reset widgets. """ self.time_le.reset() self.timestep_le.reset() self.trj_int_le.reset() if self.show_enegrp: self.enegrp_int_le.reset() if self.show_eneseq: self.eneseq_int_le.reset() if self.ensembles: self.ensemble_cb.reset() self.temp_le.reset() self.press_le.reset() if self.show_seed: self.random_seed.reset() self.save_widget.reset() self.updateTrjFrames() self.updateEneGrpFrames() self.updateEneSeqFrames() self.onEnsembleChanged()
[docs]class WaterTypesComboBox(swidgets.SLabeledComboBox): """ Combobox containing all water molecule types available in desmondutils. Replaces the user-facing 'None' with 'current'. """
[docs] def __init__(self, **kwargs): """ Create the combobox. """ water_types = [x.strip() for x in desmondutils.WATER_FFTYPES.keys()] water_types_dict = {x: x for x in water_types if x != desmondutils.NONE} water_types_dict['Current'] = desmondutils.NONE kwargs['itemdict'] = water_types_dict super().__init__('Water model:', **kwargs)
[docs]class PlaneSelectorMixin(object): """ Set of widgets to create plane and directional arrow using various methods like best fit to selected atoms, crystal vector, and plane using 3 atoms. """ NPX = numpy.array(transform.X_AXIS) NPY = numpy.array(transform.Y_AXIS) NPZ = numpy.array(transform.Z_AXIS) NPO = numpy.zeros(3, float) FIT_SELECTED_ATOMS = 'Best fit to selected atoms' CRYSTAL_VECTOR = 'Crystal vector:' CHOOSE_ATOMS = 'Choose at least 3 atoms to define the plane' PLANE_SCALE = 2.0
[docs] def addWidgetsToLayout(self, layout): """ Add PlaneSelectorMixin widgets to the passed layout :type layout: QBoxLayout :param layout: The layout to place this panel into """ self.struct = None self.modified_project = None self.previous_inclusion = [] self.modified_project = None self.temprow_id = None self.arrow_length = MINIMUM_PLANE_NORMAL_LENGTH self.arrow = None self.plane = None self.best_btext = None self.group = graphics_common.Group() self.picked_atoms = set() self.full_vector = self.NPZ.copy() self.unbuffered_origin = self.NPO.copy() self.vector_origin = self.NPO.copy() self.vector_set_by_picked_atoms = False self.method_rbg = swidgets.SRadioButtonGroup( nocall=True, command=self.methodToggled, layout=layout) self.fit_sel_atom_rb = swidgets.SRadioButton( self.FIT_SELECTED_ATOMS, tip=f'A plane {self.FIT_SELECTED_ATOMS.lower()} will be used', layout=layout) self.method_rbg.addExistingButton(self.fit_sel_atom_rb) # Crystal vectors self.cframe = swidgets.SFrame( layout=layout, layout_type=swidgets.HORIZONTAL) clayout = self.cframe.mylayout crystal_rb = swidgets.SRadioButton(self.CRYSTAL_VECTOR, layout=clayout) tip = ('The selected vector from the existing periodic boundary ' 'condition will be used.') crystal_rb.setToolTip(tip) self.cvec_frame = swidgets.SFrame( layout=clayout, layout_type=swidgets.HORIZONTAL) cvlayout = self.cvec_frame.mylayout labels = LATTICE_VECTOR_LABELS tips = [ 'Use crystal vector a', 'Use crystal vector b', 'Use crystal vector c' ] self.crystal_rbg = swidgets.SRadioButtonGroup( labels=labels, tips=tips, command=self.crystalVectorPicked, nocall=True, layout=cvlayout) clayout.addStretch() self.method_rbg.addExistingButton(crystal_rb) # Atom picking playout = swidgets.SHBoxLayout(layout=layout) self.choose_atom_rb = swidgets.SRadioButton( self.CHOOSE_ATOMS, layout=playout) tip = ('A best-fit plane for the picked atoms will be used') self.choose_atom_rb.setToolTip(tip) self.method_rbg.addExistingButton(self.choose_atom_rb) self.pick_frame = swidgets.SFrame( layout=playout, layout_type=swidgets.HORIZONTAL) pflayout = self.pick_frame.mylayout self.pick3_cb = swidgets.SCheckBox( 'Pick', layout=pflayout, checked=False) self.pick3_cb.toggled.connect(self.pickToggled) tip = ('Check this box to pick the atoms in the workspace that\n' 'define the plane. Uncheck this box when finished picking\n' 'atoms.') self.pick3_cb.setToolTip(tip) self.picker = picking.PickAtomToggle( self.pick3_cb, self.atomPicked, enable_lasso=True) btn = swidgets.SPushButton( 'Clear Selection', command=self.clearPicked, layout=pflayout) tip = ('Clear the currently picked set of atoms') btn.setToolTip(tip) playout.addStretch() self.flip_btn = swidgets.SPushButton( 'Flip Direction', command=self.flipDirection, layout=layout) tip = ('Flip the direction of the interface and the side of the\n' 'structure the interface extends from.') self.flip_btn.setToolTip(tip)
[docs] def methodToggled(self): """ React to the plane determination method being changed """ if self.struct is None: return method = self.method_rbg.checkedText() self.cvec_frame.setEnabled(method == self.CRYSTAL_VECTOR) self.pick_frame.setEnabled(method == self.CHOOSE_ATOMS) if method != self.CHOOSE_ATOMS: self.clearPicked() self.pick3_cb.reset() self.computePlaneFromEntry()
[docs] def flipDirection(self): """ Flip the direction of the interface normal """ vector = -self.full_vector if not self.vector_set_by_atoms: origin = xtal.find_origin_on_structure_exterior(self.struct, vector) else: origin = self.vector_origin self.defineNewNormal(vector=vector, origin=origin, allow_flip=False)
[docs] def pickToggled(self): """ React to a change in state of the pick atom checkbox """ if self.pick3_cb.isChecked(): self.picked_atoms = set()
[docs] def atomPicked(self, asl): """ React to the user picking another atom while defining the plane :type asl: str :param asl: The asl defining the picked atom """ struct = maestro.workspace_get() self.picked_atoms.update(analyze.evaluate_asl(struct, asl)) self.marker.setAsl( 'atom.n ' + ','.join([str(x) for x in self.picked_atoms])) self.marker.show() if len(self.picked_atoms) > 2: self.computePlaneFromAtoms(struct=struct, atoms=self.picked_atoms)
[docs] def clearPicked(self): """ Clear all the picked atom information, including the WS markers """ if self.marker: self.marker.setAsl('not all') self.picked_atoms = set()
[docs] def computePlaneFromAtoms(self, struct=None, atoms=None): """ Compute the interface plane as the best fit plane to a set of atoms :type struct: `schrodinger.structure.Structure` :param struct: The structure containing the atoms. If not given, the previously loaded structure will be used. :type atoms: list :param atoms: List of atom indexes of the atoms to fit. If not given, all atoms will be fit. """ if not struct: struct = self.struct if not atoms: atoms = maestro.selected_atoms_get() if not atoms: maestro.command('workspaceselectionreplace all') atoms = list(range(1, struct.atom_total + 1)) self.vector_set_by_atoms = False else: self.vector_set_by_atoms = True coords = numpy.array([struct.atom[a].xyz for a in atoms]) try: normal = measure.fit_plane_to_points(coords) except ValueError: self.warning('There must be at least 3 atoms for a planar ' 'interface') return except measure.LinearError: self.warning('At least one of the 3 points must not be colinear') return except numpy.linalg.LinAlgError: self.warning('Unable to find a best fit plane to these atoms') return origin = numpy.array(transform.get_centroid(struct, list(atoms))[:3]) self.defineNewNormal(vector=normal, origin=origin)
[docs] def updateArrowAndPlane(self): """ Update the workspace arrow and plane to the new coordinates. """ head = self.full_vector + self.vector_origin if not self.arrow: try: self.createArrow(head, self.vector_origin) except schrodinger.MaestroNotAvailableError: return else: self.arrow.xhead = head[0] self.arrow.yhead = head[1] self.arrow.zhead = head[2] self.arrow.xtail = self.vector_origin[0] self.arrow.ytail = self.vector_origin[1] self.arrow.ztail = self.vector_origin[2] self.createPlane()
[docs] def setStructure(self, struct): """ Set the scaffold structure that will define the interface plane :type struct: `schrodinger.structure.Structure` :param struct: The scaffold structure that will define the interface """ self.struct = struct if struct: has_props = desmondutils.has_chorus_box_props(self.struct) # Enable the crystal lattice widgets if possible self.cframe.setEnabled(has_props) if has_props: button = self.CRYSTAL_VECTOR else: button = self.FIT_SELECTED_ATOMS self.method_rbg.setTextChecked(button) self.computePlaneFromEntry() else: self.cleanUp()
[docs] def loadStructureIntoWorkspace(self): """ Put the loaded structure into the workspace so the user can view it """ # Clear out the workspace but remember what was in it ptable = maestro.project_table_get() self.previous_inclusion = [] for row in ptable.included_rows: # Saving the in_workspace value allows us to preserve fixed entries self.previous_inclusion.append((row.entry_id, row.in_workspace)) row.in_workspace = project.NOT_IN_WORKSPACE self.modified_project = ptable.fullname # Put the structure in the workspace temprow = ptable.importStructure(self.struct, wsreplace=True) self.temprow_id = temprow.entry_id
[docs] def crystalVectorPicked(self): """ React to the user choosing one of the crystal lattice vectors to define the plane """ if not self.method_rbg.checkedText() == self.CRYSTAL_VECTOR: # The group was just reset, don't react as this option isn't chosen return self.computePlaneFromEntry(use_selected_xtal_vector=True)
[docs] def computePlaneFromEntry(self, use_selected_xtal_vector=False): """ Compute the interface plane based on an entire entry. In order of preference, this would be the crystal lattice vector most parallel with the moment of inertia. If the lattice vectors are not known, then we fit a plane to the entire structure. In either case, we then move the plane so that the entry lies entirely on one side of the plane and slide the vector to be right over the centroid of the entry. :type use_selected_xtal_vector: bool :param use_selected_xtal_vector: Instead of picking the best plane based on a heirarchy of options, use the one defined by the currently selected crystal lattice vector. """ self.vector_set_by_atoms = False # Find our best guess for plane if self.method_rbg.checkedText() == self.CRYSTAL_VECTOR: if use_selected_xtal_vector: btext = self.crystal_rbg.checkedText() else: if not self.best_btext: self.setBestXtalVectorProperty() self.crystal_rbg.setTextChecked(self.best_btext) btext = self.best_btext vec = xtal.extract_chorus_lattice_vector(self.struct, btext) norm_vec = transform.get_normalized_vector(vec) vorigin = xtal.find_origin_on_structure_exterior( self.struct, norm_vec) self.defineNewNormal(origin=vorigin, vector=norm_vec) return if self.struct.atom_total < 3: self.warning('Cannot define plane for a structure with fewer ' 'than two atoms.') return try: self.computePlaneFromAtoms() except numpy.linalg.LinAlgError: # Unable to guess a plane, go with the default Z-Axis self.full_vector = self.NPZ.copy() except measure.LinearError: self.warning('Unable to define a plane for a structure with 3 ' 'co-linear atoms.') return # Pick a vector origin that is on the exterior of the structure vorigin = xtal.find_origin_on_structure_exterior( self.struct, self.full_vector) # Display the normal vector self.defineNewNormal(origin=vorigin)
[docs] def pointVectorAwayFromStructure(self, struct, vector, origin): """ Pick the 180 degree direction of the vector that points it away from the centroid of the given structure :type struct: `schrodinger.structure.Structure` :param struct: The structure to point the vector away from :type vector: `numpy.array` :param vector: The vector to potentially flip 180 :type origin: `numpy.array` :param origin: The point the vector will originate from :rtype: `numpy.array` :return: The given vector, possibly flipped 180 degrees so that it points away from the given structure """ centroid = transform.get_centroid(struct)[:3] oc_vec = centroid - origin if vector.dot(oc_vec) > 0: return -vector return vector
[docs] def createPlane(self): """ Create or update the square in the workspace that represents the interface plane """ if self.plane: self.group.remove(self.plane) self.plane = None # Find a vector perpendicular to the normal vector xaxis = self.NPX.copy() raw_perp = numpy.cross(self.full_vector, xaxis) if not transform.get_vector_magnitude(raw_perp): # Vector is parallel with the X-axis yaxis = self.NPY.copy() raw_perp = numpy.cross(self.full_vector, yaxis) # First vector in the plane four_vectors = [transform.get_normalized_vector(raw_perp)] # Second vector is perpendicular to the normal and the first vector raw_perp2 = numpy.cross(self.full_vector, four_vectors[0]) four_vectors.append(transform.get_normalized_vector(raw_perp2)) # Third vector is just 180 degrees from the first four_vectors.append(-four_vectors[0]) # Fourth vector is just 180 degrees from the second four_vectors.append(-four_vectors[1]) vertices = [] for vec in four_vectors: scaled_vec = self.PLANE_SCALE * self.arrow_length * vec + self.vector_origin # Convert to list as MaestroPolygon needs a list rather than numpy # array for each vertex vertices.append(list(scaled_vec)) # Complete the full circuit by adding the first point to the end vertices.append(vertices[0][:]) self.plane = polygon.MaestroPolygon( vertices, color='yellow', opacity=0.75) self.group.add(self.plane) self.group.show()
[docs] def createArrow(self, head_coords, tail_coords): """ Create the arrow that represents the interface plane normal in the workspace :type head_coords: `numpy.array` :param head_coords: The coordinates of the tip of the arrow :type tail_coords: `numpy.array` :param tail_coords: The coordinates of the base of the arrow """ if self.arrow: self.group.remove(self.arrow) self.arrow = None hx, hy, hz = head_coords tx, ty, tz = tail_coords radius = MINIMUM_PLANE_NORMAL_LENGTH / 10.0 self.arrow = arrow.MaestroArrow( xhead=hx, yhead=hy, zhead=hz, xtail=tx, ytail=ty, ztail=tz, color='orange', radius=radius) self.group.add(self.arrow) self.group.show()
[docs] def defineNewNormal(self, vector=None, origin=None, allow_flip=True): """ Store the new normal vector and origin for the interface plane, optionally updating the workspace graphics to show the new vector/plane :type vector: `numpy.array` :param vector: The new xyz values of the plane normal vector. If not given, the previous vector will be used. :type origin: `numpy.array` :param origin: The new origin of the vector. If not given, the previous origin will be used. :type allow_flip: bool :param allow_flip: Whether to potentially flip the vector 180 degrees so that it points away from the structure """ # This just makes a bigger arrow for bigger scaffolds - helps it be more # visible norm_len = self.struct.atom_total / 100.0 if self.struct else 0 self.arrow_length = max(MINIMUM_PLANE_NORMAL_LENGTH, norm_len) if not self.struct: return if vector is None: vector = self.full_vector if origin is None: origin = self.unbuffered_origin vector = numpy.array(vector) normvec = transform.get_normalized_vector(vector) self.unbuffered_origin = origin origin = origin + self.getBuffer() * normvec if allow_flip: normvec = self.pointVectorAwayFromStructure(self.struct, normvec, origin) self.full_vector = self.arrow_length * normvec self.vector_origin = origin self.updateArrowAndPlane()
[docs] def setBestXtalVectorProperty(self): """ Set the crystal lattice vector that is most parallel with the largest moment of inertia of the loaded structure in best_btext. This vector most likely aligns with the desired interface plane normal vector. """ inertial_vec = analyze.get_largest_moment_normalized_vector( struct=self.struct, massless=True) # Now find the crystal lattice vector that is most parallel (or # antiparallel) with the largest moment of inertia largest_dotp = -1. for btext in LATTICE_VECTOR_LABELS: vec = xtal.extract_chorus_lattice_vector(self.struct, btext) norm = transform.get_normalized_vector(vec) # Larger dot product == more parallel dotp = abs(inertial_vec.dot(norm)) if dotp > largest_dotp: largest_dotp = dotp best_btext = btext self.best_btext = best_btext
[docs] def cleanUp(self): """ Clean up the everything in the workspace from this dialog, including restoring the molecules that were in the workspace prior to it opening. Also resets some properties to their default values """ def _final_cleanup(): self.temprow_id = None self.modified_project = None self.previous_inclusion = [] self.cleanArrowAndPlanes() self.picker.stop() try: ptable = maestro.project_table_get() except (schrodinger.MaestroNotAvailableError, project.ProjectException): _final_cleanup() return if ptable.fullname == self.modified_project and self.temprow_id: # Delete our temporary project entry and reload the workspace state row = ptable.getRow(self.temprow_id) # It's possible the row might no longer exist if the user has closed # a temporary project (closing a temporary project doesn't change # the current project name, so the above project.fullname check # will still pass) or if the user has manually deleted our # temporary entry. if row: row.in_workspace = project.NOT_IN_WORKSPACE ptable.deleteRow(self.temprow_id) # This update prevents a stale row from sticking around in the PT ptable.update() for eid, state in self.previous_inclusion: row = ptable.getRow(entry_id=eid) if row: row.in_workspace = state _final_cleanup()
[docs] def cleanArrowAndPlanes(self): """ Remove the arrow and markers from the workspace """ self.group.hide() self.group.clear() self.arrow = None self.plane = None self.best_btext = None self.marker.hide() self.marker.setAsl('not all')
[docs] def resetFrame(self): """ Reset the dialog widgets and clean up the workspace """ self.cleanUp() # Reset default values self.arrow_length = MINIMUM_PLANE_NORMAL_LENGTH self.full_vector = self.NPZ.copy() self.vector_origin = self.NPO.copy() self.pick3_cb.reset() if self.struct: self.loadStructureIntoWorkspace() if self.cframe.isEnabled(): button = self.CRYSTAL_VECTOR else: button = self.FIT_SELECTED_ATOMS self.crystal_rbg.reset() self.method_rbg.setTextChecked(button) self.methodToggled()
[docs]class LipidImporter(swidgets.SFrame): """ Manage importing a forcefield supported lipid into the Maestro workspace. """ lipidImported = QtCore.pyqtSignal()
[docs] def __init__(self, layout=None, label=None, command=None): """ Create an instance. :type layout: QLayout or None :param layout: the layout to which this widget will be added or None if there isn't one :type label: str or None :param label: the label of the button or None if the default is to be used :type command: function or None :param command: a function to call on lipid import or None if none to call """ super().__init__(layout_type=swidgets.HORIZONTAL, layout=layout) self.st_dict = desmondutils._get_lipid_ff_st_dict() label = label or 'Import Lipid' swidgets.SPushButton( label, layout=self.mylayout, command=self.importLipid) items = sorted(self.st_dict.keys()) self.lipid_combo = swidgets.SComboBox( items=items, nocall=True, layout=self.mylayout) self.mylayout.addStretch() if command: self.lipidImported.connect(command)
[docs] def getStructure(self): """ Return the structure for the chosen lipid. :rtype: schrodinger.structure.Structure :return: the structure """ title = self.lipid_combo.currentText() return self.st_dict[title]
[docs] def importLipid(self): """ Import the lipid into the Maestro workspace. """ if not maestro: return struct = self.getStructure() p_table = maestro.project_table_get() p_table.importStructure(struct, wsreplace=True) self.lipidImported.emit()
[docs] def reset(self): """ Reset. """ self.lipid_combo.reset()
[docs]class Stepper(swidgets.SFrame): """ A set of widgets that allow inputting a start, stepsize and number of points """ StepperData = namedtuple('StepperData', ['start', 'num', 'stepsize']) ADD_TIP = ('This increment will be added to the previous\n' 'step value to get the new step value.') MULT_TIP = ('This multiplier will be multiplied times the previous\n' 'step value to get the new step value.')
[docs] def __init__(self, label, units, start, points, step, parent_layout, multiple=False): """ Create a Stepper instance :param str label: The label for the line of widgets :param str units: The label to put after the starting and stepsize value widgets :param float start: The initial starting value :param int points: The initial number of points :param float step: The initial stepsize :param `swidgets.SBoxLayout` parent_layout: The layout to place the Stepper into :param bool multiple: Whether the step is a multiplier (True) or additive (False) """ super().__init__(layout_type=swidgets.HORIZONTAL, layout=parent_layout) layout = self.mylayout swidgets.SLabel(label, layout=layout) self.multiple = multiple # Start st_dator = swidgets.SNonNegativeRealValidator(bottom=0.01, decimals=2) self.start_edit = swidgets.SLabeledEdit( 'Start:', edit_text=str(start), after_label=units, always_valid=True, validator=st_dator, stretch=False, width=60, layout=layout) # Number of points self.num_sb = swidgets.SLabeledSpinBox( 'Number of steps:', minimum=1, maximum=999, value=points, stretch=False, nocall=True, command=self.numPointsChanged, layout=layout) # Stepsize step_dator = swidgets.SNonNegativeRealValidator() if self.multiple: step_what = 'multiplier' step_unit = None tip = self.MULT_TIP else: step_what = 'increment' step_unit = units tip = self.ADD_TIP self.step_edit = swidgets.SLabeledEdit( f'Step {step_what}:', edit_text=str(step), after_label=step_unit, always_valid=True, validator=step_dator, stretch=False, width=40, layout=layout, tip=tip) self.numPointsChanged(self.num_sb.value()) layout.addStretch(1000)
[docs] def numPointsChanged(self, value): """ React to the number of points changing :param int value: The current number of points """ self.step_edit.setEnabled(value != 1)
[docs] def getData(self): """ Get the current settings :rtype: `StepperData` :return: The current settings """ return self.StepperData( start=self.start_edit.text(), num=self.num_sb.text(), stepsize=self.step_edit.text())
[docs] def reset(self): """ Reset the widgets """ self.start_edit.reset() self.num_sb.reset() self.step_edit.reset()
[docs] def values(self): """ A generator for the values produced by the current settings :rtype: generator of float :return: Each item generated is a value defined by the current settings """ data = self.getData() start = float(data.start) step = float(data.stepsize) num = int(data.num) for stepnum in range(num): if self.multiple: yield start * step**stepnum else: yield start + step * stepnum
[docs]class AtomNameWidgetItem(QtWidgets.QTableWidgetItem): """ QTableWidgetItem validated as type provided. """
[docs] def __init__(self, value, type_value=str, default=''): """ Initialize item object. :param value: Input value :param type_value: type to which value must be converted. Default type of value is str :param default: Value default, must be of type type_value """ assert isinstance(default, type_value) self.type_value = type_value self.old_value = default self.old_value = self.getValue(value) super().__init__( str(self.old_value), QtWidgets.QTableWidgetItem.UserType)
[docs] def getValue(self, value): """ Get value of the correct type and save it. :param value: Input value to be converted to the typed value :return: typed value :rtype: type """ try: self.old_value = self.type_value(value) except ValueError: pass return self.old_value
[docs] def setData(self, role, value): """ Validates table cell data value by converting it to float. :type role: int :param role: Item role. :param value: Value of the item's data. """ if role != QtCore.Qt.UserRole: # Update the value only if it is not user data value = self.getValue(value) super().setData(role, value)
[docs] def userData(self): """ Get user data. :rtype: any or None :return: User data or None """ return super().data(QtCore.Qt.UserRole)
def __lt__(self, other): """ Sort based on the atom index (stored in self.old_value). """ # if value does not exist if not self.old_value or not other.old_value: return False # get the atom index for values self_idx = msutils.get_index_from_default_name(self.old_value) other_idx = msutils.get_index_from_default_name(other.old_value) if self_idx is None or other_idx is None: return False return self_idx < other_idx
[docs]class MultiComboWithCount(multi_combo_box.MultiComboBox): """ A MultiComboBox that shows the number of selected items as the combobox text instead of item names """
[docs] def __init__(self, text=None, item_name='item', layout=None, items=None, command=None, stretch=True, width=80, **kwargs): """ Create a MultiComboWithCount instance :param `QBoxLayout` layout: The layout for this widget :param list items: The items to add to the combobox :param callable command: The command to call when the selection changes """ super().__init__(**kwargs) self.item_name = item_name if items: self.addItems(items) self.frame = swidgets.SFrame( layout_type=swidgets.HORIZONTAL, layout=layout) rlayout = self.frame.mylayout if text: self.label = swidgets.SLabel(text, layout=rlayout) rlayout.addWidget(self) if stretch: rlayout.addStretch() self.setMinimumWidth(width) self.popupClosed.connect(self.changeToolTip) self.selectionChanged.connect(self.recordChange) self.has_new_selection = False self.command = command
[docs] def recordChange(self, *args): """ Record that the selection has changed """ self.has_new_selection = True
[docs] def currentText(self): """ Override the parent class to only show the number of selected items rather than all the item names :rtype: str :return: The text to display in the combobox """ num_selected = len(self.getSelectedItems()) if not num_selected: return 'None' else: noun = INFLECT_ENGINE.plural(self.item_name, num_selected) return f'{num_selected} {noun}'
[docs] def changeToolTip(self): """ Change the tooltip to show all selected items """ selected = self.getSelectedItems() tip = self._delimiter.join(selected) self.setToolTip(tip) if self.has_new_selection: if self.command: self.command() self.has_new_selection = False
[docs] def hideWidgets(self, hidden): """ Hide (or show) the combobox and clear the selection :param bool hidden: Whether to hide the combobox or show it """ self.frame.setVisible(not hidden) if hidden: self.clearSelection() self.changeToolTip()
[docs]def fill_table_with_data_frame(table, data_frame): """ Fill the passed QTableWidget with data from the passed pandas data frame :param QTableWidget table: The table to fill :param `pandas.DataFrame` data_frame: The data frame to read values from """ num_rows = data_frame.shape[0] columns = data_frame.columns table.setColumnCount(len(columns)) table.setHorizontalHeaderLabels(columns) table.setRowCount(0) table.setSortingEnabled(False) for row in range(num_rows): table.insertRow(row) for col in range(len(columns)): item = swidgets.STableWidgetItem( editable=False, text=str(data_frame.iat[row, col])) table.setItem(row, col, item) table.setSortingEnabled(True)
[docs]@contextmanager def disable_sort(table): """ CM to disable sorting on a table. :type table: QTableWidget :param table: the table """ # see MATSCI-9505 - sorting should be disabled while rows are changing table.setSortingEnabled(False) try: yield finally: table.setSortingEnabled(True)