Source code for schrodinger.ui.qt.forcefield.forcefield
"""
Contains Force Field selector widgets
"""
import enum
from contextlib import contextmanager
from typing import Optional
import schrodinger
from schrodinger.forcefield import OPLS_DIR_ENV
from schrodinger.forcefield.custom_params import upgrade_oplsdir
from schrodinger.infra import mm
from schrodinger.infra.mm import \
    get_preference_use_custom_opls as get_use_custom_forcefield_preference
from schrodinger.Qt import QtWidgets
from schrodinger.ui import maestro_ui
from schrodinger.ui.qt import messagebox
from schrodinger.utils.env import EnvironmentContext
maestro = schrodinger.get_maestro()
maestro_hub = maestro_ui.MaestroHub.instance()
MIGRATED_MESSAGE = (
    'Your custom force field parameters have been automatically migrated to'
    ' the customizations file associated with the current release.\n\n'
    'Note that any new customizations you define will only be available with'
    ' this version of the Schrodinger Suite, as older versions of Maestro will'
    ' continue to use the older parameter files.')
SAVE_RESPONSE_KEY = 'use_default_ff'
PROJECT_PREFER_COMMAND = 'projectprefer'
PROJECT_OPLS_DIR_CMD_OPTION = 'projectoplsdirectory'
_no_maestro_project_opls_path = ''
[docs]def get_custom_opls_dir():
    """
    Returns current project opls path if present, else
    returns global preference custom opls path.
    :return: path to the OPLS directory.
        no validation occurs for returned directories.
    :rtype: str
    """
    return get_project_preference_custom_opls_path(
    ) or mm.get_preference_custom_opls_path() 
F14_DISP_NAME = mm.opls_version_to_name(mm.OPLSVersion.F14, mm.OPLSName_DISPLAY)
F16_DISP_NAME = mm.opls_version_to_name(mm.OPLSVersion.F16, mm.OPLSName_DISPLAY)
FFS_INCOMPATIBLE_PARAMS_MSG = f"""<html><p>Your customizations
        <a style="text-decoration:none;color:#3D3DF9;" href="opls_path">(path)</a>
        are either unavailable or incompatible with the {F16_DISP_NAME} force
        field currently in use in Schrödinger suite. New custom parameters are
        needed to accurately model your structures.</p>
        <p>You may run this job without customizations, but we recommend
        that you cancel now and generate the new parameters. Use the Force
        Field Builder, available from a Customize button on your panel or
        from the Task Tool.</p>"""
[docs]def has_valid_custom_OPLS_preference():
    """
    :return: whether the custom OPLS directory is a valid S-OPLS path
    :rtype: bool
    """
    opls_dir = get_custom_opls_dir()
    return opls_dir is None or mm.is_valid_opls_directory(opls_dir) 
[docs]class OPLSDirResult(enum.IntEnum):
    """
    Return object for validate_opls_dir function
    """
    ABORT = 0  # Cancel
    VALID = 1  # opls_dir is fine, use it
    DEFAULT = 2  # opls_dir is incompatible, use the default opls dir 
[docs]class OPLSValidator:
    """
    Validates OPLS-related settings. Meant for use by GUIs that may need to
    block other code while performing validation.
    """
    INCOMPATIBLE_PARAMS_MSG = FFS_INCOMPATIBLE_PARAMS_MSG
[docs]    def __init__(self, parent: QtWidgets.QWidget):
        """
        :ivar parent: The parent object from which to display messageboxes
        :vartype parent: QWidget
        """
        self.parent = parent
        self._validating = False 
    @property
    def validating(self) -> bool:
        """
        Return whether or not the validator is currently performing validation.
        """
        return self._validating
[docs]    def validateOPLSDir(self,
                        opls_dir: Optional[str] = None,
                        allow_default_dialog: bool = True) -> OPLSDirResult:
        """
        Return validation results of OPLS_DIR. If invalid, attempt to update the
        OPLS_DIR. If invalid and can't update OPLS_DIR, optionally prompt
        `ValidateOplsDirMessageBox`. See validate_opls_dir() for full
        documentation.
        :param opls_dir: Path to the custom OPLS directory. Use to avoid calling
            get_custom_opls_dir() twice in the event that the function is
            already called outside of this method. If not given or `None` the
            validation will be done for the user's preferences custom OPLS
            directory.
        :param allow_default_dialog: whether the user may be presented with the
            dialog allowing them to run with the default OPLS dir.
        """
        if opls_dir is None:
            opls_dir = get_custom_opls_dir()
        self._validating = True
        try:
            valid = validate_opls_dir(
                opls_dir,
                parent=self.parent,
                incompatible_params_msg=self.INCOMPATIBLE_PARAMS_MSG,
                allow_default_dialog=allow_default_dialog)
        finally:
            self._validating = False
        return valid  
[docs]@contextmanager
def use_custom_opls_dir_preference():
    """
    If specified, sets the custom OPLSDIR Maestro preference into the env in a
    context managed scope.
    """
    if get_use_custom_forcefield_preference():
        with EnvironmentContext(OPLS_DIR_ENV, get_custom_opls_dir()):
            yield
    else:  # do not set OPLS_DIR_ENV
        yield 
[docs]class ValidateOplsDirMessageBox(messagebox.MessageBox):
    """
    A message box allowing the user to cancel when the custom opls dir is not
    valid, or run with the default opls dir.
    A check box exists to allow unset the Maestro preference for using the
    custom opls dir.
    :cvar title: dialog window title
    :vartype title: str
    :cvar text: the message text
    :vartype text: str
    :ivar cancel_btn: the cancel button
    :vartype cancel_btn: QtWidgets.QPushButton
    :ivar run_defaults_btn: the 'Run with Defaults' button
    :vartype run_defaults_btn: QtWidgets.QPushButton
    :ivar incompatible_params_msg: the message to display when incompatible
        parameters are found.
    :vartype incompatible_params_msg: str
    """
    TITLE = "Custom Parameters Incompatible"
    CB_TEXT = 'Do not show this message again'
[docs]    def __init__(
        self,
        opls_dir,
        parent=None,
        incompatible_params_msg=FFS_INCOMPATIBLE_PARAMS_MSG,
    ):
        super().__init__(parent=parent,
                         save_response_key=SAVE_RESPONSE_KEY,
                         add_prefix_to_key=False)
        # Set add_prefix_to_key to False to ensure that the "Do not show"
        # state is shared between all force field selector subclasses.
        self.save_response_chk.setText(self.CB_TEXT)
        self.opls_dir = opls_dir
        self.setIcon(QtWidgets.QMessageBox.Question)
        self.setWindowTitle(self.TITLE)
        self._incompatible_params_msg = incompatible_params_msg
        self.setText(self._incompatible_params_msg)
        # find the QLabel that has our text and hook up the link call back
        for label in (lbl for lbl in self.children()
                      if isinstance(lbl, QtWidgets.QLabel)):
            if label.text() == self._incompatible_params_msg:
                break
        label.setOpenExternalLinks(False)
        label.linkActivated.connect(self._showPath)
        label.linkHovered.connect(self._showPath)
        self.run_defaults_btn = self.addButton('Run with Defaults',
                                               QtWidgets.QMessageBox.AcceptRole)
        cancel_btn = self.addButton('Cancel', QtWidgets.QMessageBox.RejectRole)
        self.setDefaultButton(cancel_btn)
        self.setEscapeButton(cancel_btn)
        self.layout().setVerticalSpacing(20)
        self.run_defaults_btn.setMinimumWidth(100) 
    def _showPath(self):
        """
        Callback method to show the tool tip for the link in the text
        """
        QtWidgets.QToolTip.showText(self.cursor().pos(), self.opls_dir)
[docs]    def getResponse(self):
        if self.clickedButton() == self.run_defaults_btn:
            return True
        return None  
[docs]def validate_opls_dir(opls_dir,
                      parent=None,
                      incompatible_params_msg=FFS_INCOMPATIBLE_PARAMS_MSG,
                      allow_default_dialog=True):
    """
    Determine whether the automatically upgraded OPLS_DIR is valid.
    Display a message box to inform the user whether an automatic upgrade was
    performed.
    If the OPLS dir remains invalid and `allow_default_dialog` is True a dialog
    allowing the user to use the default OPLS dir is presented.
    :param opls_dir: the opls directory path or None
    :type opls_dir: str or None
    :param parent: the QtWidget used to post dialogs
    :type parent: QtWidgets.QtWidget
    :param incompatible_params_msg: the message to display when incompatible
        parameters are found. Default is for `ValidateOplsDirMessageBox` to use
        FFS_INCOMPATIBLE_PARAMS_MSG.
    :type incompatible_params_msg: str
    :param allow_default_dialog: whether the user may be presented with the
        `ValidateOplsDirMessageBox` allowing them to run with the default OPLS
        dir.
    :type allow_default_dialog: bool
    :return: the validation result
    :rtype: OPLSDirResult
    """
    if opls_dir is None or mm.is_valid_opls_directory(opls_dir):
        return OPLSDirResult.VALID
    try:
        upgrade_oplsdir(opls_dir)
    except RuntimeError:
        pass
    else:
        messagebox.show_info(parent, MIGRATED_MESSAGE)
        return OPLSDirResult.VALID
    if not allow_default_dialog:
        return OPLSDirResult.ABORT
    msg_box = ValidateOplsDirMessageBox(opls_dir, parent,
                                        incompatible_params_msg)
    response = msg_box.exec_()
    if response:
        return OPLSDirResult.DEFAULT
    return OPLSDirResult.ABORT 
[docs]def get_project_preference_custom_opls_path():
    """
    Returns project specific opls directory.
    :return: path to custom OPLS parameters stored in the project-specific
        preferences
    :rtype: str
    """
    if not maestro:
        return _no_maestro_project_opls_path
    return maestro.get_command_option(PROJECT_PREFER_COMMAND,
                                      PROJECT_OPLS_DIR_CMD_OPTION) 
[docs]def set_project_preference_custom_opls_path(project_opls_path):
    """
    Sets project specific opls custom paramteres path.
    :param project_opls_path: current project oplsdir path
    :type project_opls_path: str
    """
    if not maestro:
        global _no_maestro_project_opls_path
        _no_maestro_project_opls_path = project_opls_path
        return
    cmd = f'{PROJECT_PREFER_COMMAND} {PROJECT_OPLS_DIR_CMD_OPTION}="{project_opls_path}"'
    maestro.command(cmd)