Source code for schrodinger.application.bindingsite.intfield

'''
Logic to compute "interaction fields" between (protein) atoms and
"probe" particles.
'''

import math

import numpy as np
import scipy.spatial as spatial

from schrodinger.utils import log

from . import atomtyping
from . import common

#------------------------------------------------------------------------------#

# Interactions between the probes and protein atom types. Negative/positive
# values represent favorable/unfavorable interactions with the "probes":
# "arm" -- aromatic, "hyd" -- hydrophobic, "acc" -- h-bond acceptor,
# "don" -- h-bond donor, "pos"/"neg" -- positive/negative charge.

INTERACTIONS = {}

# yapf: disable
INTERACTIONS['C.2']   = {}                                # noqa: E221
INTERACTIONS['C.3']   = {'hyd': -1}                       # noqa: E221
INTERACTIONS['C.ar']  = {'arm': -1}                       # noqa: E221
INTERACTIONS['C.cat'] = {'arm': -1, 'neg': -1}
INTERACTIONS['N.3']   = {}                                # noqa: E221
INTERACTIONS['N.4']   = {'hyd':  1, 'acc': -1, 'neg': -1} # noqa: E221,E241
INTERACTIONS['N.am']  = {'hyd':  1, 'acc': -1}            # noqa: E221,E241
INTERACTIONS['N.his'] = {'hyd':  1, 'arm': -1, 'acc': -1, 'don': -1, 'neg': -1} # noqa: E241
INTERACTIONS['N.pl3'] = {'hyd':  1, 'acc': -1}            # noqa: E241
INTERACTIONS['O.2']   = {'hyd':  1, 'don': -1}            # noqa: E221,E241
INTERACTIONS['O.3']   = {'hyd':  1, 'acc': -1, 'don': -1} # noqa: E221,E241
INTERACTIONS['O.co2'] = {'hyd':  1, 'don': -1, 'pos': -1} # noqa: E241
INTERACTIONS['S.3']   = {'hyd': -1, 'arm': -1}            # noqa: E221
INTERACTIONS['m.2']   = {'hyd':  1, 'arm': -1, 'neg': -1.25, 'acc': -1, 'pos': 0.5}   # +2 metal ion
INTERACTIONS['N.ar']  = {'arm': -1}                       # aromatic N
INTERACTIONS['P.3']   = {}                                # phosphate P
# yapf: enable

ATYPES = sorted(INTERACTIONS.keys())
PROBES = sorted({k for x in INTERACTIONS.values() for k in x.keys()})

# (default) thresholds for the interactions to be taken into account

THRESHOLDS = {
    'hyd': -0.0185,
    'arm': -0.0500,
    'don': -0.0250,
    'acc': -0.0250,
    'pos': -0.0315,
    'neg': -0.0315
}

#------------------------------------------------------------------------------#


[docs]def get_hbond_direction(atom): ''' Returns direction of "ideal" hydrogen bond that would be formed by the given atom. It is not always right, e.g. for O in C-O-H. This needs to be refined or proven irrelevant. ''' bonded = [a for a in atom.bonded_atoms if a.atomic_number > 1] if not bonded: # O in HOH bonded = [a for a in atom.bonded_atoms] if not bonded: return None start = np.zeros(3) for peer in bonded: start += np.asarray(peer.xyz) start /= len(bonded) vec = np.asarray(atom.xyz) - start veclen = np.linalg.norm(vec) if veclen > 0.0: vec /= veclen return vec
#------------------------------------------------------------------------------#
[docs]def get_interaction_sites(st, atom_indices=None, probes=None, logger=None): ''' Identifies interaction sites among the protein atoms according to their atom types. :param st: Structure. :type st: `schrodinger.structure.Structure` :param atom_indices: Iterable over the contributing atom indices. :type atom_indices: iterable over int :param probes: Probes of interest. :type probes: container of str :return: List of atoms that interact with the requested probes. Individual atom contributions are given by tuples that hold the -1/0/1 integers (see `INTERACTIONS`) associated with the corresponding probe. :rtype: list(tuple(schrodinger.structure._StructureAtom, tuple(int))) ''' logger = common.ensure_logger(logger) if atom_indices is None: atom_indices = range(1, st.atom_total + 1) if probes is None: probes = PROBES sites = [] typer = atomtyping.AtomTyper() for (i, typ) in typer(st, atom_indices=atom_indices, logger=logger): if typ is None: logger.warning('no atom type for atom %d (%s/%s)', i, st.atom[i].pdbname, st.atom[i].pdbres) continue try: params = INTERACTIONS[typ] except KeyError: continue data = tuple(params.get(p, 0) for p in probes) if any(data): sites.append((st.atom[i], data)) return sites
#------------------------------------------------------------------------------#
[docs]class Field(object): ''' Handles computation of the "interaction potentials" generated by the "interaction sites" (protein atoms) acting on "probes". '''
[docs] def __init__(self, st, atom_indices=None, probes=None, alpha=1.0, r_cut=4.0, a_cut=60.0, logger=None): ''' :param st: Structure. :type st: `schrodinger.structure.Structure` :param atom_indices: Iterable over the contributing atom indices. :type atom_indices: iterable over int :param probes: Probes of interest. :type probes: container of str :param alpha: Interaction range (length scale of exponential decay). :type alpha: float :param r_cut: Ignore contributions from atoms further than `r_cut` from a probe. :type r_cut: float :param a_cut: Ignore hydrogen bond interactions for angles exceeding `a_cut`. :type a_cut: float ''' self.probes = tuple(PROBES if probes is None else probes) if atom_indices is None: atom_indices = range(1, st.atom_total + 1) sites = get_interaction_sites( st, atom_indices, probes=self.probes, logger=logger) if not sites: raise ValueError('no interaction sites found') self.alpha = alpha self.r_cut = r_cut self.a_cut = a_cut self.hbdir = np.zeros((len(sites), 3)) self.params = np.ndarray((len(sites), len(self.probes))) self.positions = np.ndarray((len(sites), 3)) try: self._acc_index = self.probes.index('acc') except ValueError: self._acc_index = None try: self._don_index = self.probes.index('don') except ValueError: self._don_index = None for (i, (atom, data)) in enumerate(sites): self.params[i] = data self.positions[i] = atom.xyz need_direction = ( (self._acc_index is not None and data[self._acc_index]) or (self._don_index is not None and data[self._don_index])) if need_direction: self.hbdir[i] = get_hbond_direction(atom) self.kdt = spatial.cKDTree(self.positions)
def __call__(self, pos): ''' Evaluates potentials at `pos`. :return: Potentials acting on probes at position `pos`. :rtype: list(float) ''' num_probes = len(self.probes) energy = [0.0] * len(self.probes) sources = self.kdt.query_ball_point(pos, self.r_cut) for s in sources: delta = np.asarray(pos) - self.positions[s] r = np.linalg.norm(delta) cosine = np.dot(delta, self.hbdir[s]) if r > 0: cosine /= r angle = math.degrees(math.acos(cosine)) ksi = r / self.alpha for i in range(num_probes): check_direction = i in (self._acc_index, self._don_index) if check_direction and angle > self.a_cut: continue energy[i] += self.params[s][i] * math.exp(-ksi) return energy
[docs] def nearest_atom_distance(self, pos): ''' Returns distance to the nearest atom that contributes to the potentials. ''' return self.kdt.query(pos)[0]
#------------------------------------------------------------------------------#