Source code for schrodinger.application.steps.docking
import shlex
from rdkit import Chem
from schrodinger import stepper
from schrodinger.application.glide import http_client
from schrodinger.application.inputconfig import InputConfig
from schrodinger.models import parameters
from schrodinger.structutils.smiles import STEREO_FROM_ANNOTATION_AND_GEOM
from schrodinger.structutils.smiles import SmilesGenerator
from schrodinger.tasks import tasks
from schrodinger.utils import license
from . import utils
from .basesteps import MaeMapStep
from .basesteps import MolMapStep
from .dataclasses import ScoredMol
from .dataclasses import ScoredSmiles
from .dataclasses import ScoredSmilesSerializer
try:
    from schrodinger.application.glide.packages.startup import DockingJob
except ImportError:
    DockingJob = None
LICENSE_BY_NAME = {v: k for k, v in license.LICENSE_NAMES.items()}
INF = float('inf')
DOCKING_SCORE_KEY = 'r_i_docking_score'
GLIDE_SERVER_WAIT_TIME = 2.0
GLIDE_SERVER_STARTUP_WAIT_TIME = 300  # seconds
[docs]class GlideSettings(parameters.CompoundParam):
    """
    The `glide_in_file` input file should not have a `LIGANDFILE` keyword.
    The `POSES_PER_LIG` will be overridden with the value of 1, since we need to
    know whether the molecule can dock, and what the best docking score is.
    """
    glide_grid_file: stepper.StepperFile  # required
    glide_ref_ligand_file: stepper.StepperFile  # optional
    glide_in_file: stepper.StepperFile  # optional
[docs]    def validate(self, step):
        """
        Validate the settings for use in `step`.
        :param step: stepper._BaseStep
        :rtype: list[TaskError or TaskWarning]
        """
        issues = []
        for attr, required in (('glide_grid_file', True),
                               ('glide_ref_ligand_file', False),
                               ('glide_in_file', False)):  # yapf:disable
            issues += utils.validate_file(step, attr, required=required)
        return issues  
class _GlideServerStartTask(tasks.BlockingFunctionTask):
    """
    A task that starts up a glide server that will only return one pose for
    every ligand that is docked
    """
    input: GlideSettings
    output: http_client.GlideServerManager = None
    def _getKeywords(self):
        keywords = InputConfig(self.input.glide_in_file or {})
        keywords['GRIDFILE'] = self.input.glide_grid_file
        keywords['POSES_PER_LIG'] = 1
        if self.input.glide_ref_ligand_file:
            keywords['CORE_RESTRAIN'] = True
            keywords['REF_LIGAND_FILE'] = self.input.glide_ref_ligand_file
        return keywords
    def getRequiredLicenses(self):
        docking_job = DockingJob(self._getKeywords(), 'foo')
        required_licenses = {}
        for rec in docking_job.licenseRequirements():
            name, count = rec.split(':')
            required_licenses[LICENSE_BY_NAME[name]] = int(count)
        return required_licenses
    def mainFunction(self):
        keywords = self._getKeywords()
        server = http_client.GlideServerManager(keywords=keywords, use_jc=False)
        server.start(wait=GLIDE_SERVER_STARTUP_WAIT_TIME)
        self.output = server
class _MolDockerMixin:
    """
    Mixin for a `stepper._BaseStep` providing functionality for glide docking.
    To use:
       define `GLIDE_SERVER_START_TASK_CLASS`
       settings should be `GlideSettings`, so that it has a `validation` method
       Override `_dock` to do the actual docking for the `Mol` object.
    """
    GLIDE_SERVER_START_TASK_CLASS = None
    Settings = GlideSettings
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._docker = None
    def getLicenseRequirements(self):
        startup_task = _GlideServerStartTask()
        startup_task.input.glide_in_file = self.settings.glide_in_file
        return startup_task.getRequiredLicenses()
    def validateSettings(self):
        return self.settings.validate(self)
    def getDocker(self):
        """
        :return: the singleton glide server manager
        :rtype: http_client.GlideServerManager
        """
        if self._docker is None:
            startup_task = self.GLIDE_SERVER_START_TASK_CLASS()
            utils.update_params(startup_task.input, self.settings)
            startup_task.start()
            startup_task.wait()
            if startup_task.status != tasks.Status.DONE:
                raise RuntimeError('Glide docking server could not be started')
            self._docker = startup_task.output
        return self._docker
    def _dock(self, mol):
        """
        A generator of ScoredMol objects of docked molecules with a score value
        less than or equal to `max_score` in the settings.
        :param mol: the molecule to dock
        :type mol: Chem.Mol
        :return: the generator of docked ScoredMol objects
        :rtype: collections.Generator[ScoredMol]
        """
        raise NotImplementedError
    def cleanUp(self):
        super().cleanUp()
        if self._docker:
            self._docker.stop(wait=GLIDE_SERVER_WAIT_TIME)
            self._docker = None
[docs]class GlideDocker(_MolDockerMixin, MolMapStep):
    """
    Perform a glide docking step.
    Only yields the original input molecule if at least 1 pose is found with
    a score less than or equal to the settings' `max_score`.
    Note: it is not the docked `Mol` in the ScoredMol that is yielded.
    """
    GLIDE_SERVER_START_TASK_CLASS = _GlideServerStartTask
[docs]    class Settings(GlideSettings):
        max_score: float = INF 
    def _dock(self, mol):
        st = utils.mol_to_structure(mol, self, generate_coordinates=True)
        if st is not None:
            docker = self.getDocker()
            docked_sts = list(docker.dock(st))
            if docked_sts:
                score = docked_sts[0].property[DOCKING_SCORE_KEY]
                if score <= self.settings.max_score:
                    yield ScoredMol(mol=mol, score=score)
[docs]    def mapFunction(self, mol):
        for scored_mol in self._dock(mol):
            yield scored_mol.mol  
[docs]class MaeGlideDocker(_MolDockerMixin, MaeMapStep):
    """
    Perform a glide docking step, yielding the best scored pose for the input
    structure.
    """
    GLIDE_SERVER_START_TASK_CLASS = _GlideServerStartTask
[docs]    class Settings(GlideSettings):
        max_score: float = INF 
[docs]    def mapFunction(self, struc):
        docker = self.getDocker()
        docked_sts = list(docker.dock(struc))
        if docked_sts:
            docked_st = docked_sts[0]
            if docked_st.property[DOCKING_SCORE_KEY] <= self.settings.max_score:
                yield docked_st  
[docs]class SmilesDockerSettings(GlideSettings):
    arg_string: str = '-bff 16 -epik -s 32'
    ligprep_filter_file: stepper.StepperFile
[docs]    def validate(self, step):
        issues = utils.validate_file(step, 'ligprep_filter_file')
        return issues + super().validate(step)  
class _SmilesGlideServerStartTask(_GlideServerStartTask):
    """
    A task that starts up a glide server that will only return one pose for
    every ligand that is docked using ligprep and SMILES docking.
    """
    input: SmilesDockerSettings
    def _getKeywords(self):
        keywords = super()._getKeywords()
        keywords['LIGPREP'] = 'yes'
        flt_file = self.input.ligprep_filter_file
        lp_args = self.input.arg_string
        if flt_file:
            lp_args += f' -f {shlex.quote(flt_file)}'
        keywords['LIGPREP_ARGS'] = lp_args
        return keywords
[docs]class SmilesDocker(_MolDockerMixin, MolMapStep):
    """
    Perform a ligprep with glide docking step. This step will yield the
    molecules and their glide score value as `ScoredMol` objects only for
    molecules that had a score that is less than or equal to the `max_score`
    in the settings.
    Since to ligprep may generate different tautomers the same molecule may be
    yielded more than once.
    """
    GLIDE_SERVER_START_TASK_CLASS = _SmilesGlideServerStartTask
[docs]    class Settings(SmilesDockerSettings):
        max_score: float = INF 
[docs]    def setUp(self):
        super().setUp()
        self._smiles_generator = SmilesGenerator(
            STEREO_FROM_ANNOTATION_AND_GEOM, unique=True) 
    def _dock(self, mol):
        smi = Chem.MolToSmiles(mol)
        docker = self.getDocker()
        for st in docker.dockSmiles(smi):
            score = st.property[DOCKING_SCORE_KEY]
            if score <= self.settings.max_score:
                mol = utils.structure_to_mol(st, self, mol)
                yield ScoredMol(mol=mol, score=score)
[docs]    def mapFunction(self, mol):
        for scored_mol in self._dock(mol):
            yield scored_mol.mol  
[docs]class ScoredSmilesDocker(SmilesDocker):
    """
    A SmilesDocker that returns ScoredSmiles objects.
    """
    Output = ScoredSmiles
    OutputSerializer = ScoredSmilesSerializer
[docs]    def mapFunction(self, mol):
        for scored_mol in self._dock(mol):
            yield ScoredSmiles(smiles=Chem.MolToSmiles(scored_mol.mol),
                               score=scored_mol.score)