"""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