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: with Smasher.load(smasher_fname) as model: model.check_version() 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]}"