Source code for schrodinger.application.jaguar.gui.tabs.solvation_tab

import warnings
from collections import OrderedDict
from math import pi
from past.utils import old_div

from schrodinger.infra import mm
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets

from .. import ui
from ..utils import ALL_SOLVENTS
from ..utils import JaguarSettingError
from ..utils import JaguarSettingWarning
from .base_tab import BaseTab


def different(value1, value2):
    """
    Return True if the given float values are different
    from each other (tiny differences are not considered).
    """
    return abs(value1 - value2) > 1e-10


class SolventTab(BaseTab):
    """
    A base class for the Solvent tab (used in the pKa panel) and the standard
    Solvation tab.
    """

    NAME = "Solvent"
    HELP_TOPIC = "JAGUAR_TOPIC_SOLVATION_FOLDER"

    def _warnAboutOtherSolventTypes(self, jag_input):
        """
        Warn if any keywords for the "other" solvent type are set, since they
        will be ignored.

        :param jag_input: The Jaguar settings to check
        :type jag_input: `schrodinger.application.jaguar.input.JaguarInput`

        """
        NON_ALLOWED_KEYS = (mm.MMJAG_RKEY_EPSOUT, mm.MMJAG_RKEY_RADPRB)
        bad_keys = [
            key for key in NON_ALLOWED_KEYS if jag_input.isNonDefault(key)
        ]
        if bad_keys:
            plural_s = "s" if len(bad_keys) > 1 else ""
            msg = (
                "Other solvent type keyword%s %s not available for this task" %
                (plural_s, " and ".join(bad_keys)))
            warnings.warn(JaguarSettingWarning(msg))


class SolventPkaTab(SolventTab):
    """
    The Solvent tab for the Pka panel.
    """

    SOLVENTS = {"Water": mm.MMJAG_SOLVENT_WATER, "DMSO": mm.MMJAG_SOLVENT_DMSO}
    UI_MODULES = (ui.solvent_pka_tab_ui,)

    def setup(self):
        super().setup()
        self.ui.solvent_combo.addItemsFromDict(self.SOLVENTS)

    def getMmJagKeywords(self):
        keywords = super().getMmJagKeywords()
        solv_type = self.ui.solvent_combo.currentData()
        keywords[mm.MMJAG_SKEY_SOLVENT] = solv_type
        ipka_solv_opt = mm.MMJAG_IPKA_SOLV_OPT_OFF
        if self.ui.use_solvation_cb.isChecked():
            ipka_solv_opt = mm.MMJAG_IPKA_SOLV_OPT_ON
        keywords[mm.MMJAG_IKEY_IPKA_SOLV_OPT] = ipka_solv_opt
        # Explicitly clear any dielectric and probe radius settings, since
        # they're incompatible with the solvents selectable in this tab
        keywords[mm.MMJAG_RKEY_EPSOUT] = None
        keywords[mm.MMJAG_RKEY_RADPRB] = None
        return keywords

    def loadSettings(self, jag_input):
        solv_type = jag_input[mm.MMJAG_SKEY_SOLVENT]
        try:
            self.ui.solvent_combo.setCurrentData(solv_type)
        except ValueError:
            msg = "Unrecognized solvent type %s=%s" % (mm.MMJAG_SKEY_SOLVENT,
                                                       solv_type)
            warnings.warn(JaguarSettingWarning(msg))
        self._warnAboutOtherSolventTypes(jag_input)
        ipka_solv_opt = jag_input[mm.MMJAG_IKEY_IPKA_SOLV_OPT]
        ipka_solv_opt_bool = ipka_solv_opt == mm.MMJAG_IPKA_SOLV_OPT_ON
        self.ui.use_solvation_cb.setChecked(ipka_solv_opt_bool)


class SolvationTab(SolventTab):
    """
    The Solvation tab, which includes solvent model and gas-phase reference
    energy
    """

    NAME = "Solvation"
    UI_MODULES = (ui.solvent_tab_ui,)
    NO_SOLVENT_MODEL = "None"
    PBF_MODEL = "PBF"
    PCM_MODEL = "PCM"
    SM6_MODEL = "SM6"
    SM8_MODEL = "SM8"
    # SM6 and SM8 models are not available yet, so we will only show
    # the first two items at this time.  NOTE - when they are implemented,
    # remove their special handling in the SolvationTabNoOptimized class.
    SOLVENT_MODELS = OrderedDict([
        (NO_SOLVENT_MODEL, mm.MMJAG_ISOLV_OFF),
        (PBF_MODEL, mm.MMJAG_ISOLV_PBF),
        (PCM_MODEL, mm.MMJAG_ISOLV_PCM),
        #(SM6_MODEL, mm.MMJAG_ISOLV_SM6),
        #(SM8_MODEL, mm.MMJAG_ISOLV_SM8),
    ])

    CPCM_PCM_MODEL = "CPCM"
    COSMO_PCM_MODEL = "COSMO"
    SSVPE_PCM_MODEL = "SS(v)PE"
    PCM_MODELS = OrderedDict([(CPCM_PCM_MODEL, mm.MMJAG_PCM_MODEL_CPCM),
                              (COSMO_PCM_MODEL, mm.MMJAG_PCM_MODEL_COSMO),
                              (SSVPE_PCM_MODEL, mm.MMJAG_PCM_MODEL_SSVPE)])

    BONDI_PCM_RADII = "Bondi"
    UFF_PCM_RADII = "UFF"
    KLAMT_PCM_RADII = "Klamt"
    PCM_RADII = OrderedDict([(BONDI_PCM_RADII, mm.MMJAG_PCM_RADII_BONDI),
                             (UFF_PCM_RADII, mm.MMJAG_PCM_RADII_UFF),
                             (KLAMT_PCM_RADII, mm.MMJAG_PCM_RADII_KLAMT)])

    def setup(self):
        super().setup()
        self.ui.solvent_model_combo.addItemsFromDict(self.SOLVENT_MODELS)
        self.ui.solvent_model_combo.currentIndexChanged.connect(
            self.solventModelChanged)
        self.ui.pcm_model_combo.addItemsFromDict(self.PCM_MODELS)
        self.ui.pcm_radii_combo.addItemsFromDict(self.PCM_RADII)

        self.gas_phase_group = QtWidgets.QButtonGroup(self)
        self.gas_phase_group.addButton(self.ui.optimized_structure_rb)
        self.gas_phase_group.addButton(self.ui.input_structure_rb)

        self.ui.solvent_chooser_btn.solventChanged.connect(
            self.ui.solvent_le.setText)
        self.solventModelChanged()

    def solventModelChanged(self):
        """
        Called when a new item is selected in the solvent model type menu.
        """

        isolv = self.ui.solvent_model_combo.currentText()

        enable = isolv in (self.PBF_MODEL, self.PCM_MODEL, self.SM8_MODEL)
        self.ui.solvent_lbl.setEnabled(enable)
        self.ui.solvent_le.setEnabled(enable)
        self.ui.solvent_chooser_btn.setEnabled(enable)
        if not enable:
            self.ui.solvent_chooser_btn.setSolvent(mm.MMJAG_SOLVENT_WATER)

        visible = (isolv == self.PCM_MODEL)
        self.ui.pcm_frame.setVisible(visible)

        visible = isolv in (self.PBF_MODEL, self.PCM_MODEL)
        self.ui.gas_phase_frame.setVisible(visible)

    def getMmJagKeywords(self):

        keywords = {}
        keywords.update(self._getSolventModelAndTypeKeyword())
        keywords.update(self._getPCMKeywords())
        keywords.update(self._getGasPhaseKeywords())
        return keywords

    def _getSolventModelAndTypeKeyword(self):
        """
        Get the solvent model keyword

        :return: A dictionary of keywords
        :rtype: dict
        """

        isolv = self.ui.solvent_model_combo.currentData()
        solvent_type = self.ui.solvent_chooser_btn.getSolvent()
        return {mm.MMJAG_IKEY_ISOLV: isolv, mm.MMJAG_SKEY_SOLVENT: solvent_type}

    def _getPCMKeywords(self):
        """
        Get PCM keywords when applicable.

        :return: A dictionary of keywords
        :rtype: dict
        """

        keywords = {}
        isolv = self.ui.solvent_model_combo.currentText()
        if isolv != self.PCM_MODEL:
            keywords[mm.MMJAG_SKEY_PCM_MODEL] = None
            keywords[mm.MMJAG_SKEY_PCM_RADII] = None
            keywords[mm.MMJAG_IKEY_PBF_AFTER_PCM] = None
            return keywords
        pcm_model = self.ui.pcm_model_combo.currentData()
        keywords[mm.MMJAG_SKEY_PCM_MODEL] = pcm_model
        pcm_radii = self.ui.pcm_radii_combo.currentData()
        keywords[mm.MMJAG_SKEY_PCM_RADII] = pcm_radii
        pbf_after_pcm = mm.MMJAG_PBF_AFTER_PCM_OFF
        if self.ui.pbf_optimization_cb.isChecked():
            pbf_after_pcm = mm.MMJAG_PBF_AFTER_PCM_ON
        keywords[mm.MMJAG_IKEY_PBF_AFTER_PCM] = pbf_after_pcm

        return keywords

    def _getGasPhaseKeywords(self):
        """
        Get the gas phase reference energy keywords

        :return: A dictionary of keywords
        :rtype: dict
        """

        keywords = {}
        isolv = self.ui.solvent_model_combo.currentText()
        if isolv not in (self.PBF_MODEL, self.PCM_MODEL):
            keywords[mm.MMJAG_IKEY_NOGAS] = None
            return keywords
        if self.ui.optimized_structure_rb.isChecked():
            nogas = mm.MMJAG_NOGAS_OFF
        elif self.ui.input_structure_rb.isChecked():
            nogas = mm.MMJAG_NOGAS_ZMAT
        keywords[mm.MMJAG_IKEY_NOGAS] = nogas
        return keywords

    def loadSettings(self, jag_input):
        self._checkSolventSettings(jag_input)
        self._warnAboutOtherSolventTypes(jag_input)
        self._loadSolventModelAndTypeSettings(jag_input)
        self._loadPCMSettings(jag_input)
        self._loadGasPhaseSettings(jag_input)

    def _checkSolventSettings(self, jag_input):
        """
        Make sure that the solvent settings are consistent.  Issue warnings for
        any inconsistencies.

        :param jag_input: The Jaguar settings to check
        :type jag_input: `schrodinger.application.jaguar.input.JaguarInput`
        """

        SETTING_KEYS = (mm.MMJAG_RKEY_EPSOUT, mm.MMJAG_RKEY_RADPRB)
        SOLVENT_KEYS = (
            (mm.MMJAG_SKEY_SOLVENT, mm.MMJAG_IKEY_NOGAS) + SETTING_KEYS)

        if jag_input[mm.MMJAG_IKEY_ISOLV] not in (mm.MMJAG_ISOLV_PBF,
                                                  mm.MMJAG_ISOLV_PCM,
                                                  mm.MMJAG_ISOLV_SM8):
            bad_keys = [
                key for key in SOLVENT_KEYS if jag_input.isNonDefault(key)
            ]
            if bad_keys:
                msg = ("Solvent settings specified but solvation not set to "
                       "PBF or PCM or SM8 (%s != %i or %i).  The following "
                       "keys will be ignored: %s." %
                       (mm.MMJAG_IKEY_ISOLV, mm.MMJAG_ISOLV_PBF,
                        mm.MMJAG_ISOLV_SM8, ", ".join(bad_keys)))
                warnings.warn(JaguarSettingWarning(msg))
                return

        if jag_input[mm.MMJAG_SKEY_SOLVENT] != "other":
            bad_keys = [
                key for key in SETTING_KEYS if jag_input.isNonDefault(key)
            ]
            if bad_keys:
                msg = ("Solvent settings specified but solvent not set to "
                       "other (%s != other).  The following keys will be "
                       "ignored: %s." % (mm.MMJAG_SKEY_SOLVENT,
                                         ", ".join(bad_keys)))
                warnings.warn(JaguarSettingWarning(msg))

    def _loadSolventModelAndTypeSettings(self, jag_input):
        """
        Load the solvent model and solvent type settings.  If solvent model is
        not PBF, then a solvent type of water is used regardless of the
        `jag_input` setting.

        :param jag_input: The Jaguar settings to load
        :type jag_input: `schrodinger.application.jaguar.input.JaguarInput`
        """

        self.ui.solvent_model_combo.setCurrentMmJagData(
            jag_input, mm.MMJAG_IKEY_ISOLV, "solvent model")

        if jag_input[mm.MMJAG_IKEY_ISOLV] in (mm.MMJAG_ISOLV_PBF,
                                              mm.MMJAG_ISOLV_SM8):
            solvent = jag_input[mm.MMJAG_SKEY_SOLVENT]
            try:
                self.ui.solvent_chooser_btn.setSolvent(solvent)
            except ValueError:
                msg = ("Unrecognized solvent type "
                       f"{mm.MMJAG_SKEY_SOLVENT}={solvent}")
                warnings.warn(JaguarSettingWarning(msg))
        else:
            self.ui.solvent_chooser_btn.setSolvent(mm.MMJAG_SOLVENT_WATER)

    def _loadPCMSettings(self, jag_input):
        """
        Load PCM settings.
        """

        self.ui.pcm_model_combo.setCurrentMmJagData(
            jag_input, mm.MMJAG_SKEY_PCM_MODEL, "pcm model")
        self.ui.pcm_radii_combo.setCurrentMmJagData(
            jag_input, mm.MMJAG_SKEY_PCM_RADII, "pcm radii")
        pbf_after_pcm = jag_input[mm.MMJAG_IKEY_PBF_AFTER_PCM]
        self.ui.pbf_optimization_cb.setChecked(
            pbf_after_pcm == mm.MMJAG_PBF_AFTER_PCM_ON)

    def _loadGasPhaseSettings(self, jag_input):
        """
        Load the gas phase reference energy settings

        :param jag_input: The Jaguar settings to load
        :type jag_input: `schrodinger.application.jaguar.input.JaguarInput`
        """

        if (jag_input[mm.MMJAG_IKEY_ISOLV] != mm.MMJAG_ISOLV_PBF):
            return

        nogas = jag_input[mm.MMJAG_IKEY_NOGAS]

        # NOGAS_DEFAULT == -1, and NOGAS_OFF == 0, but the default behavior is
        # NOGAS_OFF.
        if nogas == mm.MMJAG_NOGAS_OFF or nogas == mm.MMJAG_NOGAS_DEFAULT:
            self.ui.optimized_structure_rb.setChecked(True)
        elif nogas == mm.MMJAG_NOGAS_ZMAT:
            self.ui.input_structure_rb.setChecked(True)
        else:
            msg = ("Gas-phase reference energy setting not recognized (%s=%f)" %
                   (mm.MMJAG_IKEY_NOGAS, nogas))
            warnings.warn(JaguarSettingWarning(msg))


class SolvationTabNoOptimized(SolvationTab):
    """
    A solvation tab that doesn't allow "Optimized gas-phase structure".
    """

    def setup(self):
        super(SolvationTabNoOptimized, self).setup()
        self.ui.optimized_structure_rb.setText("Input gas-phase structure")
        self.ui.input_structure_rb.hide()
        self.ui.pbf_optimization_cb.hide()
        # SM6 and SM8 models are only available for jobs without
        # optimization (SPE & Rigid CS), so add them here.
        self.ui.solvent_model_combo.addItem(self.SM6_MODEL, mm.MMJAG_ISOLV_SM6)
        self.ui.solvent_model_combo.addItem(self.SM8_MODEL, mm.MMJAG_ISOLV_SM8)

    def _loadGasPhaseSettings(self, jag_input):
        if (jag_input[mm.MMJAG_IKEY_ISOLV] == mm.MMJAG_ISOLV_PBF and
                jag_input[mm.MMJAG_IKEY_NOGAS] == mm.MMJAG_NOGAS_ZMAT):
            msg = ("%s=%i not allowed for single point calculations." %
                   (mm.MMJAG_IKEY_NOGAS, jag_input[mm.MMJAG_IKEY_NOGAS]))
            warnings.warn(JaguarSettingWarning(msg))
        else:
            super(SolvationTabNoOptimized,
                  self)._loadGasPhaseSettings(jag_input)