Source code for schrodinger.application.steps.utils
import os
import uuid
from rdkit import Chem
from schrodinger import stepper
from schrodinger.structutils.smiles import SmilesGenerator
from schrodinger.thirdparty import rdkit_adapter
from schrodinger.utils import log
try:
    from ligand_ml.smasher import Smasher
except:
    Smasher = None
logger = log.get_output_logger('application.steps')
logger.setLevel(log.INFO)
[docs]class StepsError(RuntimeError):
    pass 
[docs]def to_string(obj):
    """
    Convert the mol or scored mol to a string representation.
    To be used in logging and file writing.
    :param obj: a step input or output object
    :type obj: Chem.Mol or object
    :return: text representation
    :rtype: str
    """
    if isinstance(obj, Chem.Mol):
        return Chem.MolToSmiles(obj)
    return str(obj) 
[docs]def mol_to_structure(mol, step, generate_coordinates=False):
    """
    Convert a mol to a structure object catching and reporting exceptions.
    :param mol: the mol to convert
    :type mol: Chem.Mol
    :param step: Step this conversion is being performed for
    :type step: stepper._BaseStep
    :param generate_coordinates: whether coordinates should be generated
    :type generate_coordinates: bool
    :return: the structure or None if there were issues
    :rtype: structure.Structure or NoneType
    """
    try:
        return rdkit_adapter.from_rdkit(
            mol,
            include_properties=False,
            generate_coordinates=generate_coordinates)
    except Exception as e:
        logger.error(
            f'ERROR: Mol to structure conversion in {step.getStepId()} for'
            f' {Chem.MolToSmiles(mol)}:\n{str(e)}') 
[docs]def structure_to_mol(st, step, input_mol=None):
    """
    Convert a structure to a mol object catching and reporting exceptions.
    :param st: the structure to convert
    :type st: Structure.Structure
    :param step: Step this conversion is being performed for
    :type step: stepper._BaseStep
    :param input_mol: the input molecule for this step
    :type mol: Chem.Mol
    :return: the molecule or None if there were issues
    :rtype: Chem.Mol or NoneType
    """
    try:
        return rdkit_adapter.to_rdkit(st,
                                      implicitH=True,
                                      include_properties=False,
                                      include_coordinates=False,
                                      sanitize=True)
    except Exception as e:
        logger.error(
            f'ERROR: Structure to mol conversion in {step.getStepId()} for'
            f' {Chem.MolToSmiles(input_mol)}:\n{str(e)}')
        # try to create a Mol using the st's SMILES, which is expected
        smiles_generator = SmilesGenerator()
        st_smi = smiles_generator.getSmiles(st)
        mol = Chem.MolFromSmiles(st_smi)
        if mol:
            mol_smi = Chem.MolToSmiles(mol)
            if mol_smi != st_smi:
                logger.warning(f'using {mol_smi} not {st_smi}')
            else:
                logger.warning(f'using {mol_smi}')
        return mol 
[docs]def fragment_to_molecule(fragment):
    """
    Create a molecule from a fragment with implicit H.
    :param fragment: the fragment
    :type fragment: Chem.Mol
    :return: the molecule version of the fragment
    :rtype: Chem.Mol
    """
    mol = Chem.Mol(fragment)
    for atom in mol.GetAtoms():
        if atom.GetAtomicNum() == 0:
            atom.SetAtomicNum(1)
    # fix if have >= 2 Hs on a center that is flagged as chiral
    mol = Chem.RemoveHs(mol)
    Chem.AssignStereochemistry(mol, cleanIt=True)
    return mol 
[docs]def validate_core_smarts(step, core_smarts):
    """
    Validation of a required core smarts for a step.
    :param step: The step that the validation is for
    :type step: stepper.BaseStep
    :param core_smarts: the SMARTS to validate
    :type core_smarts: str or NoneType
    :return: the list of validation issues
    :rtype: List[stepper.ValidationIssue]
    """
    if core_smarts is None:
        return [stepper.SettingsError(step, 'core_smarts must be set')]
    if Chem.MolFromSmarts(core_smarts) is None:
        return [stepper.SettingsError(step, 'invalid core_smarts')]
    return [] 
RESOURCE_TYPE_ERROR_MAP = {
    stepper.ResourceType.LOCAL: stepper.LocalResourceError,
    stepper.ResourceType.STATIC: stepper.StaticResourceError,
}
[docs]def validate_file(step, attr_name, required=False):
    """
    Validate a step's stepper file setting.
    If the file setting is required but not defined a stepper.SettingsError
    will be generated.
    Depending on the file setting's resource type, a stepper.StaticResourceError
    or stepper.LocalResourceError will be generated.
    :param step: the step that the validation is for
    :type step: stepper._BaseStep
    :param attr_name: the settings attribute name
    :type attr_name: str
    :param required: whether the file has to be defined
    :type required: bool
    :return: the list of validation issues
    :rtype: List[stepper.ValidationIssues]
    """
    return _validate_resource(step,
                              attr_name,
                              required,
                              existence_check_func=os.path.isfile) 
[docs]def validate_folder(step, attr_name, required=False):
    return _validate_resource(step,
                              attr_name,
                              required,
                              existence_check_func=os.path.isdir) 
def _validate_resource(step, attr_name, required, existence_check_func):
    file_param = step.Settings.getSubParam(attr_name)
    file_resource = file_param.getParamValue(step.settings)
    if not file_resource and required:
        return [stepper.SettingsError(step, f'{attr_name} must be set')]
    if file_resource:
        what = f'{attr_name} "{file_resource}"'
        error_type = RESOURCE_TYPE_ERROR_MAP[file_resource.resource_type]
        if not existence_check_func(file_resource):
            return [error_type(step, f'{what} not found')]
        if (file_resource.resource_type is file_resource.STATIC and
                not os.path.isabs(file_resource)):
            return [error_type(step, f'{what} should have an absolute path')]
    return []
[docs]def apply_config_settings_to_step(config_dict, step):
    """
    Applies all possible items from settings to the configuration settings of
    the step.
    :param config_dict: the configuration dictionary
    :type config_dict: dict
    :param step: the steps to apply the settings to
    :type step: stepper.BaseStep
    """
    if step.settings is None:
        return
    step_settings = step.settings.toDict()
    for k, v in config_dict.items():
        if k in step_settings:
            step_settings[k] = v
    step._applyConfigSettings(step_settings) 
[docs]def update_params(to_params, from_params):
    """
    Update the to_params with the values in from_params.
    :param to_params: the parameters to update
    :type to_params: parameters.CompoundParam
    :param from_params: the parameters to get the values from
    :type from_params: parameters.CompoundParam
    """
    to_dict = to_params.toDict()
    from_dict = from_params.toDict()
    update_dict = {k: v for k, v in from_dict.items() if k in to_dict}
    to_params.setValue(update_dict) 
[docs]def validate_smasher_file(smasher_fname):
    """
    Validate a Smasher input archive
    :param smasher_fname: Filename of the Smasher input to validate
    :type smasher_fname: str
    :return: A string error if issues are encountered, None otherwise.
    :rtype: str or NoneType
    """
    if not smasher_fname.endswith('.tar.gz') and not smasher_fname.endswith(
            '.qzip'):
        # Smasher auto-attempts to load other extensions as directories.
        return "Smasher archives must end with .qzip or .tar.gz"
    if Smasher is None:
        if not os.path.isfile(smasher_fname):
            return "Smasher archive does not exist"
        # the local system can not verify the smasher input archive so we assume
        # that there are no problems with it
        return
    try:
        Smasher.load(smasher_fname)
    except Exception as err:
        return f"Error loading Smasher model: {str(err)}" 
[docs]def need_Hs(mol, sma):
    """
    Determine whether the SMARTS of the underlying `sma` has explicit hydrogens,
    so that a molecule that has only implicit H's needs to have hydrogens added
    in order to be able to have a substructure match.
    :param mol: the mol to check
    :type mol: Chem.Mol
    :param sma: the smarts mol to check against
    :type sma: Chem.Mol
    :return: True if addHs is needed, False if it wasn't needed and None if it
        can not be determined, i.e., SMARTS never matched
    :rtype: bool or NoneType
    """
    mol = Chem.RemoveHs(mol)
    if mol.HasSubstructMatch(sma):
        return False
    if Chem.AddHs(mol).HasSubstructMatch(sma):
        return True
    return None 
[docs]def generate_stepid_and_random_suffix(step):
    return f"{step.getStepId()}_{str(uuid.uuid4())[:8]}"