Source code for schrodinger.application.phase.packages.shape_generator

"""
Supplies consistent means of generating shape structures via ligand and
pharmacophore command line options.

Copyright Schrodinger LLC, All Rights Reserved.
"""

import argparse
import enum
import os

from schrodinger import shape
from schrodinger.application.phase import hypothesis
from schrodinger.application.phase.packages import phase_utils
from schrodinger.infra import phase
from schrodinger.utils import fileutils
from schrodinger.utils import log

# Logging
logger = log.get_output_logger('shape.shape_generator')

# Shape type options
SHAPE_PHARM_COLOR = "pharm"
SHAPE_ATOM_COLOR = "atom_color"
SHAPE_ATOM_NO_COLOR = "atom_no_color"

SHAPE_TYPE_DEFAULT = SHAPE_PHARM_COLOR

# Ligand atom typing options
ATOM_TYPE_ARGS = ["mmod", "element", "qsar"]
AtomTypes = enum.Enum("AtomTypes", ATOM_TYPE_ARGS)
ATOM_TYPE_SCHEME = {
    None: shape.AtomTypeScheme_MACROMODEL,  # Atom type default
    AtomTypes.mmod.name: shape.AtomTypeScheme_MACROMODEL,
    AtomTypes.element.name: shape.AtomTypeScheme_ELEMENT,
    AtomTypes.qsar.name: shape.AtomTypeScheme_PHASE_QSAR,
}


[docs]class ShapeGenerator():
[docs] def __init__(self, args): """ Initializes shape generation options. :param args: argument namespace with command line options :type args: argparser.Namespace """ # Color is enabled for all pharmacophore-based screens, as well as # when -atomtypes is mmod, element, or qsar for ligand-based screens self.color_enabled = args.shape_type == SHAPE_ATOM_COLOR # Ligand options self._atom_type_scheme = ATOM_TYPE_SCHEME[args.atomtypes] self._hydrogen_treatment = shape.HydrogenTreatment_NONE if args.hydrogens: self._hydrogen_treatment = shape.HydrogenTreatment_POLAR # Pharmacophore options self._use_pharmacophore = args.shape_type == SHAPE_PHARM_COLOR self._feature_definitions = self.getFeatureDefinitions(args) self._site_mapper = phase.PhpSiteMapper(self._feature_definitions) self._feature_radii = self.getHypothesisFeatureRadii(args.rad) self._projected_treatment = shape.ProjectedSiteTreatment_TYPED # Force pharmacophore based shapes if the template is .phypo if fileutils.is_hypothesis_file(args.shape): self._use_pharmacophore = True # Always enable color when screening pharmacophore-based shapes if self._use_pharmacophore: self.color_enabled = True
[docs] def getFeatureDefinitions(self, args): """ Collects the feature definitions to use for generating pharmacophore hypothesis shapes from ligands. :param args: argument namespace with command line options :type args: argparser.Namespace :return: list of pharmacophore feature definitions :rtype: list of phase.PhpFeatureDefinition """ # User provided project = phase.PhpProject() if args.fd: return project.getFeatureDef(args.fd) # If providing a .phypo query, default to the hypothesis definitions if fileutils.is_hypothesis_file(args.shape): return hypothesis.PhaseHypothesis(args.shape).getFd() # If screening a Phase database, default to the database definitions if phase_utils.is_phase_database_path(args.screen): phdb_def_file = os.path.join(args.screen, phase.PHASE_DATABASE_FEATURE_DEFS) return project.getFeatureDef(phdb_def_file) # Otherwise just use Phase installation defaults return project.getDefaultFeatureDef()
[docs] def getHypothesisFeatureRadii(self, radii_file): """ Collects user specified pharmacophore feature radii to use for generating pharmacophore hypothesis shapes from ligands. :param radii_file: user supplied .rad filename :type radii_file: str :return: Data for feature specific radii :rtype: phase.PhpFeatureData """ # No feature radii to set if not radii_file: return None radii_data = phase.PhpFeatureData() radii_data.importFromFile(radii_file) return radii_data
[docs] def getHypothesisSitesShape(self, sites): """ Returns a shape structure created from the given Phase hypothesis. :param sites: pharmacophore sites source :type sites: list(phase.PhpSite) :return: shape structure derived from the given hypothesis :rtype: shape.ShapeStructure """ if self._feature_radii: for site in sites: site_type = site.getSiteTypeChar() site.setRad(self._feature_radii.getDataValue(site_type)) return shape.ShapeStructure(sites, self._projected_treatment)
[docs] def getShape(self, st, atom_weight_property=None): """ Creates a shape structure from the given ligand structure. :param st: ligand source structure :type st: structure.Structure :return: shape structure derived from the given structure :rtype: shape.ShapeStructure """ # Only apply atomweights to shape query structures from atoms if atom_weight_property and not self._use_pharmacophore: return shape.ShapeStructure(st, self._hydrogen_treatment, self._atom_type_scheme, atom_weight_property) return self.getConformerShapes([st])[0]
[docs] def getConformerShapes(self, conformers): """ Creates a list of shape structure from the given ligand conformers. :param conformers: ligand source conformers of the same structure :type conformers: list(structure.Structure) :return: shape structures derived from the given conformers :rtype: list(shape.ShapeStructure) """ # Bypass use of PhaseHypothesisFromLigand to optimize generating # pharmacophore sites across conformers; using PhpSiteMapper allows for # mapping without recompiling mmpatty patterns if self._use_pharmacophore: return [ self.getHypothesisSitesShape(sites) for sites in self._site_mapper.mapSites(conformers) ] # Otherwise just generate from the ligand conformers return [ shape.ShapeStructure(st, self._hydrogen_treatment, self._atom_type_scheme) for st in conformers ]
# -----------------------------------------------------------------------------
[docs]class SerializableShapeGenerator(ShapeGenerator):
[docs] def __init__(self, params): ''' Accept either command line arguments or dictionary representation. :param params: Command line arguments or dictionary representation similar to the one returned by the `toDict` method. :type params: namespace or dict ''' if isinstance(params, dict): self.fromDict(params) else: super().__init__(args=params)
[docs] def fromDict(self, data): ''' Configures the instance using data from `data`. :param data: Dictionary representation of the instance configuration. :type data: dict ''' for name in ('color_enabled', '_use_pharmacophore'): setattr(self, name, data[name]) self._atom_type_scheme = ATOM_TYPE_SCHEME[None] self._hydrogen_treatment = shape.HydrogenTreatment_NONE self._feature_radii = None self._projected_treatment = shape.ProjectedSiteTreatment_TYPED if self._use_pharmacophore: self._projected_treatment = data['_projected_treatment'] self._feature_definitions = \ phase.feature_definitions_from_string( data['_feature_definitions']) if data['_feature_radii']: radii_data = phase.PhpFeatureData() radii_data.importFromString(data['_feature_radii']) self._feature_radii = radii_data else: self._feature_definitions = \ phase.read_feature_definitions(phase.getDefaultFdFilePath()) for name in ('_atom_type_scheme', '_hydrogen_treatment'): setattr(self, name, data[name]) self._site_mapper = phase.PhpSiteMapper(self._feature_definitions)
[docs] def toDict(self): ''' Returns dictionary representation intended for JSON serialization. ''' outcome = { 'color_enabled': self.color_enabled, '_use_pharmacophore': self._use_pharmacophore, } if self._use_pharmacophore: outcome['_feature_definitions'] = \ phase.feature_definitions_to_string(self._feature_definitions) outcome['_feature_radii'] = \ self._feature_radii.exportToString() \ if self._feature_radii else None outcome['_projected_treatment'] = self._projected_treatment else: outcome['_atom_type_scheme'] = self._atom_type_scheme outcome['_hydrogen_treatment'] = self._hydrogen_treatment return outcome
# ============================================================================= # Command line # =============================================================================
[docs]def add_shape_generation_options(parser): """ Adds ligand-based screening options to the provided parser. :param parser: Argument parser object :type parser: argparser.ArgumentParser """ group = parser.add_argument_group(title="Shape Generation Options") group.add_argument( "-shape_type", choices=[SHAPE_PHARM_COLOR, SHAPE_ATOM_COLOR, SHAPE_ATOM_NO_COLOR], default=None, help=f"Treat each structure as a set of Phase pharmacophores, typed \ atoms, or untyped atoms (default: {SHAPE_PHARM_COLOR}).") # Atom typing scheme for -color similarity calculations: # MacroModel; Elemental; Phase QSAR (default: MacroModel). group.add_argument("-atomtypes", choices=ATOM_TYPE_ARGS, help=argparse.SUPPRESS) # Consider hydrogens attached to non-carbon atoms. group.add_argument("-hydrogens", action="store_true", help=argparse.SUPPRESS) # Pharmacophore feature definition file. group.add_argument("-fd", metavar="<fdFile>", help=argparse.SUPPRESS) # Pharmacophore feature radii file. group.add_argument("-rad", metavar="<radFile>", help=argparse.SUPPRESS)
[docs]def validate_shape_gen_options(args): """ Validates command-line argument pharmacophore option compatibility. :param args: argument namespace with command line options :type args: argparse.Namespace :return: tuple of validation success, and error message :rtype: bool, str """ if args.shape_type is None: args.shape_type = SHAPE_TYPE_DEFAULT if args.atomtypes or args.hydrogens: if fileutils.is_hypothesis_file(args.shape): return False, "Ligand options not valid with .phypo query" if args.shape_type == SHAPE_PHARM_COLOR: return False, "Ligand options not valid with pharmacophore shapes" if args.shape_type != SHAPE_PHARM_COLOR and (args.fd or args.rad): return False, "Pharmacophore options not valid with atom-based shapes" if args.shape_type != SHAPE_ATOM_COLOR and args.atomtypes: return False, "-atomtypes option only valid with -shape_type atom_color" return True, ""
[docs]def validate_shape_generator_dict(data): ''' Validates "serialized" (as dictionary) representation of ShapeGenerator. :param data: Dictionary representation of the instance configuration. :type data: dict :return: Validation outcome and complain (if any). :rtype: (bool, str) ''' try: SerializableShapeGenerator(data) return True, '' except KeyError as e: return False, f'"{str(e)}" is not provided' except phase.PhpException as e: return False, f'PhpException: {str(e)}'
[docs]def validate_shape_generator_dict_compatibility(meta1, meta2): ''' Validates shape generators compatibility. :param meta1: Validated shape generator dict #1. :type meta1: dict :param meta2: Validated shape generator dict #2. :type meta2: dict :return: Validation outcome and complain (if any). :rtype: (bool, str) ''' k1 = set(meta1.keys()) k2 = set(meta2.keys()) if k1 != k2: names = ','.join(sorted(k1 ^ k2)) return False, f'{names} available only in one instance' # assume valid meta1 and meta2 g1 = SerializableShapeGenerator(meta1) g2 = SerializableShapeGenerator(meta2) for attr in sorted(k1): if getattr(g1, attr) != getattr(g2, attr): return False, f'{attr} is different' return True, ''