"""
Phase driver for the Develop Common Pharmacophore Hypotheses (DHCP) workflow.
Copyright Schrodinger LLC, All Rights Reserved.
"""
import os
import subprocess
from schrodinger.application.phase import hypothesis
from schrodinger.infra import phase
from schrodinger.utils import fileutils
from schrodinger.utils import log
# Logging
logger = log.get_output_logger(__file__)
# Schrodinger env variable
SCHRODINGER_UTIL = os.path.join(os.getenv("SCHRODINGER"), "utilities")
XVOL_SHELL_DRIVER = os.path.join(SCHRODINGER_UTIL, "create_xvolShell")
XVOL_CLASH_DRIVER = os.path.join(SCHRODINGER_UTIL, "create_xvolClash")
XVOL_RECEPTOR_DRIVER = os.path.join(SCHRODINGER_UTIL, "create_xvolReceptor")
[docs]class ExcludedVolumeGenerator(object):
    """
    Class to create excluded volumes for a given PhaseHypothesis.
    """
[docs]    def __init__(self, base_hypothesis):
        """
        Initialize by creating a temp copy of the base hypothesis.
        :param base_hypothesis:
        :type base_hypothesis: str or `PhpHypoAdaptor`
        """
        self.base_hypo = base_hypothesis 
    def _getSettings(self, settings_inputconfig):
        """
        Initialize the class, updating the PhaseHypothesisInputConfig mode,
        validating the settings, and creating a member namedtuple.
        :param settings_inputconfig: Phase derived InputConfig instance
        :type settings_inputconfig: `PhaseHypothesisInputConfig`
        """
        if not settings_inputconfig:
            return None
        #settings_inputconfig.setInputMode(phase_input.InputMode.create_xvol)
        settings_inputconfig.validateInput()
        return settings_inputconfig.asNamedTuple()
[docs]    def createExcludedVolumeShell(self, active_sts, settings):
        """
        Runs the excluded volume shell creation for given active structures.
        create_xvolShell -hypo <hypo> -ref <actives> [options]
        :param active_sts: structures to build excluded volume shell around
        :type active_sts: list of `structure.Structure`
        :param settings: calculation settings
        :type settings: `PhaseHypothesisInputConfig`
        :return: Excluded volume based on active structure shell
        :rtype: `phase.PhpExclVol`
        """
        xvol_settings = self._getSettings(settings)
        if not xvol_settings.excluded_volume_mode == "shell":
            raise RuntimeError("Unexpected excluded volume mode.")
        args = []
        if xvol_settings.min_surface_to_volume_distance:
            buff_dist = xvol_settings.min_surface_to_volume_distance
            args.extend(["-buff", buff_dist])
        if xvol_settings.excluded_volume_sphere_radii:
            args.extend(["-grid", xvol_settings.excluded_volume_sphere_radii])
        if xvol_settings.append_excluded_volumes:
            args.append("-append")
        # If using a reference structure, create and run with TempStructureFile
        if active_sts:
            with fileutils.TempStructureFile(active_sts) as ref_filename:
                args = ["-ref", ref_filename] + args
                xvol = self._runCreateXvolCommand(XVOL_SHELL_DRIVER, args)
        # Otherwise, run the driver with no structure file
        else:
            xvol = self._runCreateXvolCommand(XVOL_SHELL_DRIVER, args)
        return xvol 
[docs]    def createExcludedVolumeClash(self, active_sts, inactive_sts, settings):
        """
        Runs the excluded volume clash creation for a given set of active and
        inactive structures.
        create_xvolClash -hypo <hypo> -pos <actives> -neg <inactives> [options]
        :param active_sts: active structures
        :type active_sts: list of `structure.Structure`
        :param inactive_sts: inactive structures
        :type inactive_sts: list of `structure.Structure`
        :param settings: calculation settings
        :type settings: `PhaseHypothesisInputConfig`
        :return: Excluded volume based on active/inactive clash
        :rtype: `phase.PhpExclVol`
        """
        xvol_settings = self._getSettings(settings)
        if not xvol_settings.excluded_volume_mode == "clash":
            raise RuntimeError("Unexpected excluded volume mode.")
        args = []
        if xvol_settings.min_surface_to_volume_distance:
            buff_dist = xvol_settings.min_surface_to_volume_distance
            args.extend(["-buff", buff_dist])
        if xvol_settings.min_num_inactives_with_clash:
            args.extend(["-freq", xvol_settings.min_num_inactives_with_clash])
        if xvol_settings.excluded_volume_sphere_radii:
            args.extend(["-grid", xvol_settings.excluded_volume_sphere_radii])
        if xvol_settings.append_excluded_volumes:
            args.append("-append")
        with fileutils.TempStructureFile(active_sts) as actives_file, \
                
fileutils.TempStructureFile(inactive_sts) as inactives_file:
            args = ["-pos", actives_file, "-neg", inactives_file] + args
            xvol = self._runCreateXvolCommand(XVOL_CLASH_DRIVER, args)
        return xvol 
[docs]    def createExcludedVolumeReceptor(self, receptor_st, settings):
        """
        Runs the excluded volume creation for a given receptor
        create_xvolReceptor -hypo <hypo> -receptor <receptor> [options]
        :param receptor_st: receptor structure
        :type receptor_st: `structure.Structure`
        :param settings: calculation settings
        :type settings: `PhaseHypothesisInputConfig`
        :return: Excluded volume based on active/inactive clash
        :rtype: `phase.PhpExclVol`
        """
        xvol_settings = self._getSettings(settings)
        if not xvol_settings.excluded_volume_mode == "receptor":
            raise RuntimeError("Unexpected excluded volume mode.")
        args = []
        if xvol_settings.min_surface_to_volume_distance:
            buff_dist = xvol_settings.min_surface_to_volume_distance
            args.extend(["-buff", buff_dist])
        if xvol_settings.receptor_radii_size_value:
            args.extend(["-radius", xvol_settings.receptor_radii_size_value])
        if xvol_settings.receptor_radii_size_prop:
            args.extend(["-rprop", xvol_settings.receptor_radii_size_prop])
        if xvol_settings.receptor_radii_scaling_value:
            radii_scale = xvol_settings.receptor_radii_scaling_value
            args.extend(["-scale", radii_scale])
        if xvol_settings.receptor_radii_scaling_prop:
            args.extend(["-sprop", xvol_settings.receptor_radii_scaling_prop])
        if xvol_settings.receptor_shell_limit:
            args.extend(["-limit", xvol_settings.receptor_shell_limit])
        if xvol_settings.append_excluded_volumes:
            args.append("-append")
        with fileutils.TempStructureFile([receptor_st]) as receptor_file:
            args = ["-receptor", receptor_file] + args
            xvol = self._runCreateXvolCommand(XVOL_RECEPTOR_DRIVER, args)
        return xvol 
    def _runCreateXvolCommand(self, xvol_driver, args):
        """
        Build and run the excluded volume driver. All excluded volume
        calculations rquire a hypothesis name to work from.
        :param xvol_driver: excluded volume driver name
        :type xvol_driver: str
        :param args: list of command arguments
        :type args: list
        :return: a new excluded volume based on a given hypothesis
        :rtype: `phase.PhpExclVol`
        """
        # Create temp hypothesis file for the backend
        with fileutils.tempfilename(suffix=".phypo") as temp_hypo:
            # Build command
            phase.PhpHypoAdaptor(self.base_hypo).save(temp_hypo, True)
            cmd = [xvol_driver, "-hypo", fileutils.splitext(temp_hypo)[0]]
            cmd.extend([str(a) for a in args])
            # Run the utilitiy
            try:
                subprocess.check_output(cmd, universal_newlines=True)
            except subprocess.CalledProcessError as e:
                logger.error("Command:\n" + " ".join(e.cmd))
                logger.error("Output:\n" + e.output)
                raise RuntimeError("create_xvol command has failed.")
            hypo = hypothesis.PhaseHypothesis(temp_hypo)
        return hypo.getXvol()