Source code for schrodinger.ui.qt.forcefield.ffselector

from typing import List
from typing import Optional

from schrodinger import get_maestro
from schrodinger.forcefield import OPLSVersion
from schrodinger.infra import mm
from schrodinger.infra.mm import get_preference_use_custom_opls
from schrodinger.Qt.QtCore import pyqtSignal
from schrodinger.Qt.QtCore import pyqtSlot
from schrodinger.Qt.QtWidgets import QFrame
from schrodinger.Qt.QtWidgets import QLayout
from schrodinger.ui.maestro_ui import MaestroHub
from schrodinger.ui.qt.swidgets import SHBoxLayout
from schrodinger.ui.qt.swidgets import SLabeledComboBox
from schrodinger.utils.license import is_opls2_available

from .ffcustomoplssettingwidget import FFCustomOPLSSettingWidget
from .forcefield import F14_DISP_NAME
from .forcefield import F16_DISP_NAME
from .forcefield import OPLSDirResult
from .forcefield import OPLSValidator
from .forcefield import get_custom_opls_dir
from .forcefield import has_valid_custom_OPLS_preference

MMFFS_INT = 10
MMFFS_NAME = 'MMFFs'

maestro_hub = MaestroHub.instance()
maestro = get_maestro()

_opls_available = None

MMFFS_INT = 10
MMFFS_NAME = 'MMFFs'
FALLBACK_NAME = F14_DISP_NAME

FORCE_FIELD_NUMBER_TO_NAME = {
    OPLSVersion.F14: F14_DISP_NAME,
    OPLSVersion.F16: F16_DISP_NAME,
    MMFFS_INT: MMFFS_NAME
}


[docs]class ForceFieldSelector(QFrame):
[docs] def __init__(self, layout=None, show_when_no_choice=False, parent=None, stretch=True, add_customize_force_field=True): """Initialize the force field selector widget :param layout: The layout to add this widget to :type layout: `PyQt5.QtWidgets.QLayout` :param show_when_no_choice: Whether to show the ffs even if the user has no choice of force fields :type show_when_no_choice: bool :param parent: The Qt parent :type parent: `PyQt5.QtWidgets.QWidget` :type stretch: bool :param stretch: If layout is supplied, the default behavior is to add a stretch to the layout after this Frame is added to it. Setting stretch to False prevents the stretch from being added to the layout. It has no effect if layout is not provided. :type add_customize_force_field: bool :param add_customize_force_field: Whether or not to add the customize force field widgets. """ super().__init__(parent=parent) self._custom_settings_widget = None self._opls_dir_validator = OPLSValidator(parent=self) self._show_when_no_choice = show_when_no_choice if layout is not None: if not isinstance(layout, QLayout): raise ValueError("layout is not a QLayout") layout.addWidget(self) self.layout = SHBoxLayout(self) self.force_field_menu = ffComboBox("Force field:", layout=self.layout, command=self._selectionChanged) self._populateMenu() if add_customize_force_field: self._addCustomize() self.update() self._hideIfNoChoice() if stretch: self.layout.addStretch(1)
@property def has_valid_opls_dir(self): """ Checks whether Maestro preferences point to a valid S-OPLS directory. :return: whether the OPLS dir is valid :rtype: bool """ return has_valid_custom_OPLS_preference() def _hideIfNoChoice(self): """ Hides the entire FFS if there are fewer than two force field choices available, unless show_when_no_choice is True (False by default). """ if self._show_when_no_choice: return num_options = len(self.force_field_menu) if num_options < 2: self.hide() self.setEnabled(False) def _addCustomize(self): """ Add a Customize button if there is a license for the Force Field builder panel """ if not (_is_opls_available() and mm.mmcommon_display_ffbuilder()): return self._custom_settings_widget = FFCustomOPLSSettingWidget(self) self.layout.addWidget(self._custom_settings_widget) @pyqtSlot(str) def _selectionChanged(self): """ Hooked up to the forcefield menu currentIndexChanged[str] signal. Shows or hides customize related widgets. """ if self._custom_settings_widget is None: return show_custom = self.getSelectionForceFieldInt() == OPLSVersion.F16 self._custom_settings_widget.setVisible(show_custom) def _populateMenu(self, additional_ff_items=None): """ Populate menu items based on licensing. Prepends the menu items with additional_ff_items as needed. """ menu_items = [F14_DISP_NAME] if self._includeF16(): menu_items.append(F16_DISP_NAME) if additional_ff_items is not None: menu_items[:0] = additional_ff_items for menu_item in menu_items: self.force_field_menu.addItem(menu_item) def _includeF16(self) -> bool: """ :return: whether to include the F16 force field in the menu (FFLD-2441) """ # OPLS_MAIN licence exists or SCIENTIFIC_SOLUTIONS_MENU feature flag on return mm.mmcommon_display_opls2() or mm.mmcommon_display_scisol()
[docs] def setF16Only(self): """ Reset force field menu and include only F16 force field in the menu. If OPLS license is not available exception is raised. This method is intended to be used only with panels that require OPLS license to run. """ if not self._includeF16(): raise RuntimeError(f"{F16_DISP_NAME} license unavailable") self.force_field_menu.clear() self.force_field_menu.addItem(F16_DISP_NAME)
[docs] def getSelectionForceField(self, index: Optional[int] = None) -> str: """ Return the string representation of the selected force field. If index is passed, the force field name at that index is returned. Because Maestro panels using this selector set options based on the common OPLS names, extracted names are regularized to be compatible. :param index: combo box index :return: the name corresponding to the selected force field """ if index is None: index = self.force_field_menu.currentIndex() name = self.force_field_menu.itemText(index) try: version_int = mm.opls_name_to_version(name) except IndexError: return name # leave non-OPLS alone return mm.opls_version_to_name(version_int, mm.OPLSName_COMMON)
[docs] def setSelectionForceField(self, force_field_int): """Set the force_field_menu's current text to the name matching the force field int. Will raise a key error if the int does not correspond to a possible selection and a value error if the int does not correspond to an available selection. :param force_field_int the integer corresponding to the force field :type force_field int :return: None """ force_field_name = FORCE_FIELD_NUMBER_TO_NAME[force_field_int] index = self.force_field_menu.findText(force_field_name) if index == -1: raise ValueError("{0} is not an available force field" \ .format(force_field_name)) self.force_field_menu.setCurrentIndex(index) self._selectionChanged()
[docs] def getSelectionForceFieldInt(self): """ Return the integer representation of the current force field. For names without mappable integers (i.e. those loaded by the MaestroForceFieldSelector) -1 is returned. :rtype: int """ text = self.force_field_menu.currentText() for version_int, name in FORCE_FIELD_NUMBER_TO_NAME.items(): if name == text: return version_int return -1
[docs] def getCustomOPLSDIR(self): """ Return OPLSDIR suitable for passing to jobcontrol through -OPLSDIR or None if usage of a custom directory is not selected. :rtype: str :returns: string of OPLS dir or None custom forcefield not selected """ # If the widget controlling the usage of a custom directory is # hidden we treat it as if it was not selected. if (self._custom_settings_widget is not None and self._custom_settings_widget.isUseCustomizedVersionChecked() and not self._custom_settings_widget.isHidden()): return get_custom_opls_dir() else: return None
[docs] def sanitizeCustomOPLSDir(self, allow_default_dialog=True): """ Sanitize the custom OPLS directory if a custom OPLS dir is used. If a custom OPLS directory is used that is not acceptable a dialog will be presented to allow the user to abort, or use the default parameters, see also `validate_opls_dir`. Note: A side-effect of this method call is that the custom force field checkbox may be unchecked. This means that `self.getCustomOPLSDir()` has to be called (again) if this method returns True in order to determine what the actual opls directory is that should be used. :param allow_default_dialog: whether the user may be presented with the dialog allowing them to run with the default OPLS dir. :type allow_default_dialog: bool :return: False if the custom OPLS dir choice is not acceptable. True in all other cases :rtype: bool """ opls_dir = self.getCustomOPLSDIR() if opls_dir: valid = self._opls_dir_validator.validateOPLSDir( opls_dir=opls_dir, allow_default_dialog=allow_default_dialog) if valid == OPLSDirResult.DEFAULT: self._custom_settings_widget.setUseCustomizedVersionCBState( False) return valid != OPLSDirResult.ABORT return True
[docs] def update(self): """ Synchronize the maestro preferences with the selector. """ force_field_int = mm.get_preference_opls_version() force_field_name = FORCE_FIELD_NUMBER_TO_NAME[force_field_int] index = self.force_field_menu.findText(force_field_name) # If not found, e.g. S-OPLS is not available but set in preferences if index == -1: # Fall back to OPLS_2005 (F14) index = self.force_field_menu.findText(FALLBACK_NAME) if index == -1: raise ValueError("Could not find fallback forcefield " f"{FALLBACK_NAME} in {self.force_field_menu}") self.force_field_menu.setCurrentIndex(index) if self._custom_settings_widget is not None: use_custom_pref = get_preference_use_custom_opls() self._custom_settings_widget.setUseCustomizedVersionCBState( use_custom_pref) self._selectionChanged()
[docs] def hide(self): """ Hide all the children of the QFrame object and then hide the QFrame itself. """ self.force_field_menu.hide() self.force_field_menu.label.hide() if self._custom_settings_widget is not None: self._custom_settings_widget.hide() super().hide()
[docs]class ForceFieldSelectorUi(ForceFieldSelector): """ A forcefield selector that accepts the standard QFrame initialization arguments so it can be used directly in Qt Designer by promoting a QFrame. """
[docs] def __init__(self, parent=None): super().__init__(parent=parent)
[docs]class MacrocycleForceFieldSelector(ForceFieldSelector): """ A forcefield widget that contains an entry for MMFFs """ def _populateMenu(self): """ Populate the force field dropdown with the standard force fields plus MMFFs """ super()._populateMenu() self.force_field_menu.addItem(MMFFS_NAME)
[docs]class ffComboBox(SLabeledComboBox):
[docs] def showEvent(self, event): """ Override show event to update the default value in case the user has updated maestro preferences. """ self.update() super().showEvent(event)
[docs]class MaestroForceFieldSelector(ForceFieldSelector): """ This class is intened to use only for Maestro cpp panels that uses force field selectors to set force field names Signals emitted by this class. :cvar forcefieldOptionChanged: Signal emitted when the force field selection is changed. Emitted with: - the current selection of force field - the cpp panel name in which the selector is embedded :cvar useCustomforceFieldToggled: Signal emitted when 'Use custom force field' toggle state is changed. Emitted with: - the cpp panel name in which the selector is embedded - the current toggle state """ forcefieldOptionChanged = pyqtSignal(str, str) useCustomForceFieldToggled = pyqtSignal(str, bool)
[docs] def __init__(self, panel, mmod_ffoptions=None, *args, **kwargs): """ Initialize the Maestro force field selector widget :param panel: is the cpp panel name (used with showpanel command in Maestro) :type panel: str :param mmod_ffoptions: the list of forcefield names supported by macromodel cpp panel :type mmod_ffoptions: list or None """ self.cpp_panel = panel self.mmod_ffoptions = mmod_ffoptions self.build_ffmenu = False super().__init__(*args, **kwargs) self.setObjectName(panel) self.build_ffmenu = True maestro_hub.updateMaestroFFSelection.connect( self._setMaestroFFSelection)
def _addCustomize(self): super()._addCustomize() if self._custom_settings_widget is None: return self._custom_settings_widget.useCustomParameterStateChanged.connect( self._onUseCustomFFcbToggled) def _populateMenu(self): """ Populate the force field dropdown with the standard force fields plus MMFFs """ additional_ff_items = [] if self.mmod_ffoptions is not None: additional_ff_items = self.mmod_ffoptions super()._populateMenu(additional_ff_items) def _setMaestroFFSelection(self, panel, ffname): """ Change the force field menu selection to ffname """ maestro_ffselector[panel].setForceField(ffname) @pyqtSlot(str) def _selectionChanged(self): """ Override the hooked up slot to the ForceFieldSelector """ if self.build_ffmenu: self.forcefieldOptionChanged.emit(self.cpp_panel, self.getSelectionForceField()) super()._selectionChanged()
[docs] def setForceField(self, name: str): """ Set the force_field_menu's current text to the name matching the force field name. Because Maestro panels using this selector set with values based on the common OPLS names, they must be translated to available display names. :param name: the name corresponding to the force field """ try: version_int = mm.opls_name_to_version(name) except IndexError: pass else: # For OPLS names, regularize to combobox display name name = mm.opls_version_to_name(version_int, mm.OPLSName_DISPLAY) index = self.force_field_menu.findText(name) if index == -1: raise ValueError(f"{name} is not an available force field") self.force_field_menu.setCurrentIndex(index)
[docs] def getForceFieldOptions(self) -> List[str]: """ Obtain names for all available force field choices in the selector. Because Maestro panels using this selector set options based on the common OPLS names, extracted names are regularized to be compatible. :return: all available combo box options """ ff_options = [] for i in range(self.force_field_menu.count()): ff_options.append(self.getSelectionForceField(index=i)) return ff_options
def _onUseCustomFFcbToggled(self, checked): """ Respond to the customize force field checkbox being toggled :param checked: whether the check box is on or off. :type checked: bool """ self.useCustomForceFieldToggled.emit(self.cpp_panel, checked)
# Global dict to maintain maestro force field selector widgets maestro_ffselector = {}
[docs]def get_maestro_forcefield_selector(panel, default_opls, mmod_ffoptions, show_when_no_choice=False, add_customize_force_field=True): """ This function is called from Maestro. Creates an instance of the 'MaestroForceFieldSelector' widget and passes on the QWidget to Maestro via MaestroHub::setForceFieldSelectorWidget SIGNAL. Also hooks up the SIGNAL emitted from MaestroForceFieldSelector class to MaestroHub::emitffselectorOptionChanged Slot See ForceFieldSelector for arguments documentation. :param panel: C++ panel name in which force field selector is embedded :type panel: str :param default_opls: default force field option to select :type panel: str :param mmod_ffoptions: list of forcefield options supported by given mmod panel :type mmod_ffoptions: list or None """ global maestro_ffselector maestro_ffselector[panel] = MaestroForceFieldSelector( panel, mmod_ffoptions, show_when_no_choice=show_when_no_choice, add_customize_force_field=add_customize_force_field) maestro_ffselector[panel].setForceField(default_opls) maestro_ffselector[panel].update() maestro_ffselector[panel].forcefieldOptionChanged.connect( maestro_hub.emitffselectorOptionChanged) maestro_hub.setForceFieldSelectorWidget.emit( panel, maestro_ffselector[panel], maestro_ffselector[panel].getForceFieldOptions()) maestro_ffselector[panel].useCustomForceFieldToggled.connect( maestro_hub.emitUseCustomFFStateChanged)
def _is_opls_available(): """ Determine if S-OPLS (F16) is available. """ # This is run in a function to avoid calling at import and cached to avoid # duplicate license feature checks global _opls_available if _opls_available is None: _opls_available = is_opls2_available() return _opls_available