"""Set of functionalities to run prodigy."""
import logging
from abc import ABC, abstractmethod
import numpy as np
from haddock import log
from haddock.core.exceptions import ModuleError
from haddock.core.typing import FilePath, Optional, ParamDict
from haddock.libs.libontology import PDBFile
# Define conversion constants
R_GAS_CONST = 8.31446261815324 	# (J x mol−1 x K−1)
CALS_TO_JOULES = 4.184
KELVIN_TO_CELCIUS = 273.15
[docs]
class CheckInstall:
    """Verify that installation of prodigy is ok."""
    def __init__(self):
        """Verify that installation of prodigy is ok.
        Raises
        ------
        ModuleError
            Raised when prodigy libraries could not be loaded.
        """
        try:
            self.import_prodigy_libs()
        except ModuleNotFoundError:
            raise ModuleError(
                "Issue detected with the installation of Prodigy. "
                "Consider installing it with: "
                "pip install prodigy-prot prodigy-lig"
                )
[docs]
    @staticmethod
    def import_prodigy_libs() -> None:
        """Import prodigy prot and lig libraries."""
        import prodigy_prot  # noqa : F401
        import prodigy_lig  # noqa : F401
        return None 
 
[docs]
class ProdigyWorker(ABC):
    """Prodigy Worker class."""
    def __init__(self, model: FilePath, params: ParamDict) -> None:
        # Use by both prodigy -prot and -lig
        self.model = model
        self.topKd = params["to_pkd"]
        self.temperature = params["temperature"]
        self.dist_cutoff = params["distance_cutoff"]
        # Output values
        self.score: Optional[float] = None
        self.error: Optional[Exception] = None
    def _run(self) -> None:
        """Evaluate complex and compute score."""
        # Patching the logging to not print everything on screen
        logging.getLogger("Prodigy").setLevel(logging.CRITICAL)
        # Running the predictions
        self.score = self.pkd_converter(self.evaluate_complex())
[docs]
    def run(self) -> None:
        """Execute the _run method."""
        try:
            self._run()
        except ModuleError as error:
            self.error = error 
[docs]
    def pkd_converter(self, deltaG: float) -> float:
        """Decide if deltaG must be converted to pKd.
        Parameters
        ----------
        deltaG : float
            Input DeltaG value
        Returns
        -------
        score : float
            The converted DeltaG to pKd if self.topKd is true,
            else the input DeltaG.
        """
        if self.topKd:
            score = self.deltaG_to_pKd(
                deltaG,
                kelvins=self.temperature + KELVIN_TO_CELCIUS,
                )
        else:
            score = deltaG
        return round(score, 3) 
    
[docs]
    @staticmethod
    def deltaG_to_pKd(deltaG: float, kelvins: float = 298.3) -> float:
        """Convert a deltaG (in Kcal/mol) to pKd.
        Parameters
        ----------
        deltaG : float
            DeltaG value (in Kcal/mol)
        kelvins : float, optional
            Temperature at which to perform the conversion.
            By default 298.3 (25 degrees Celcius)
        Returns
        -------
        pKd : float
            The corresponding pKd value at given temperature.
        """
        # Convert Kcals to joules
        deltaG_joules = deltaG * CALS_TO_JOULES * 1000
        # Use formula
        Kd = np.exp(-deltaG_joules / (R_GAS_CONST * kelvins))
        # Convert to pKd
        _pKd = np.log10(Kd)
        # Reduce number of decimals
        pKd = round(_pKd, 2)
        return -pKd 
[docs]
    @abstractmethod
    def evaluate_complex(self) -> float:
        """Logic to evaluate a complex using prodigy."""
        pass 
 
[docs]
class ModelScore:
    """Simple class for holding score for a model."""
    def __init__(self, model_index: int) -> None:
        """Initiate models scores."""
        self.index = model_index
        self.score = None
        self.error = None 
[docs]
class ProdigyBaseJob(ABC):
    """Managing the computation of prodigy scores within haddock3."""
    def __init__(
            self,
            model: PDBFile,
            params: ParamDict,
            index: int = 1,
            ) -> None:
        """Initiate a worker."""
        self.score = ModelScore(index)
        worker = self.get_worker()
        self.worker = worker(model.rel_path, params)
    def _run(self) -> ModelScore:
        """Run the worker and retrieve output values."""
        try:
            self.worker.run()
        except Exception as e:
            log.error(e)
            self.worker.error = str(e)
            self.score.error = str(e)
        else:
            self.score.score = self.worker.score
            self.score.error = self.worker.error
        return self.score
[docs]
    def run(self) -> ModelScore:
        """Execute the _run method."""
        return self._run() 
[docs]
    @staticmethod
    @abstractmethod
    def get_worker() -> object:
        """Return the appropriate worker."""
        pass