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