Source code for luna.interaction.type

import numpy as np

from luna.util.math import atom_coordinates, centroid
from luna.util.default_values import PYMOL_INTERACTION_COLOR_AS_RGB
from luna.util import rgb2hex


NUCLEOPHILE_INTERS = ["Orthogonal multipolar", "Parallel multipolar", "Antiparallel multipolar", "Tilted multipolar", "Multipolar",
                      "Cation-nucleophile", "Unfavorable anion-nucleophile", "Unfavorable nucleophile-nucleophile"]

ELECTROPHILE_INTERS = ["Orthogonal multipolar", "Parallel multipolar", "Antiparallel multipolar", "Tilted multipolar", "Multipolar",
                       "Anion-electrophile", "Unfavorable cation-electrophile", "Unfavorable electrophile-electrophile"]

UNFAVORABLE_INTERS = ["Repulsive", "Unfavorable anion-nucleophile", "Unfavorable cation-electrophile",
                      "Unfavorable nucleophile-nucleophile", "Unfavorable electrophile-electrophile"]


[docs]class InteractionType: """Define an interaction type. Parameters ---------- src_grp : :class:`~luna.mol.groups.AtomGroup` The interaction’s first atom or atom group. For directional interactions such as hydrogen bonds, ``src_grp`` represents donor atoms and atom groups that act as nucleophiles, i.e., from where the interaction "comes from". trgt_grp : :class:`~luna.mol.groups.AtomGroup` The interaction’s second atom or atom group. For directional interactions such as hydrogen bonds, ``trgt_grp`` represents acceptor atoms and atom groups that act as electrophiles, i.e., the interaction "receiver". inter_type : str The interaction type. src_interacting_atms : iterable of :class:`~luna.mol.atom.ExtendedAtom`, optional If provided, represent the set of atoms from ``src_grp`` that in fact participate in the interaction. For example, in hydrophobic islands, not all of their atoms are in direct contact with another hydrophobic surface. trgt_interacting_atms : iterable of :class:`~luna.mol.atom.ExtendedAtom`, optional If provided, represent the set of atoms from ``trgt_grp`` that in fact participate in the interaction. For example, in hydrophobic islands, not all of their atoms are in direct contact with another hydrophobic surface. src_centroid : array_like of float (size 3), optional Atomic coordinates (x, y, z) of the centroid of ``src_grp``. If not provided, it will be calculated automatically from ``src_grp`` or ``src_interacting_atms``. trgt_centroid : array_like of float (size 3), optional Atomic coordinates (x, y, z) of the centroid of ``trgt_grp``. If not provided, it will be calculated automatically from ``trgt_grp`` or ``trgt_interacting_atms``. directional : bool Indicate if the interaction has a direction as in hydrogen bonds and multipolar interactions. params : dict, optional Interaction parameters (distances, angles, etc). """ def __init__(self, src_grp, trgt_grp, inter_type, src_interacting_atms=None, trgt_interacting_atms=None, src_centroid=None, trgt_centroid=None, directional=False, params=None): self._src_grp = src_grp self._trgt_grp = trgt_grp src_interacting_atms = src_interacting_atms or [] self._src_interacting_atms = list(src_interacting_atms) trgt_interacting_atms = trgt_interacting_atms or [] self._trgt_interacting_atms = list(trgt_interacting_atms) self._src_centroid = np.array(src_centroid) if src_centroid is not None else None self._trgt_centroid = np.array(trgt_centroid) if trgt_centroid is not None else None self._type = inter_type self.directional = directional self._params = params or {} self._hash_cache = None self._apply_refs() self._expand_dict() @property def src_grp(self): """:class:`~luna.mol.groups.AtomGroup`: The interaction’s first atom or atom group.""" return self._src_grp @src_grp.setter def src_grp(self, atm_grp): self._src_grp = atm_grp # Reset hash. self._hash_cache = None self._apply_refs() @property def trgt_grp(self): """:class:`~luna.mol.groups.AtomGroup`: The interaction’s second atom or atom group.""" return self._trgt_grp @trgt_grp.setter def trgt_grp(self, atm_grp): self._trgt_grp = atm_grp # Reset hash. self._hash_cache = None self._apply_refs() @property def src_interacting_atms(self): """iterable of :class:`~luna.mol.atom.ExtendedAtom`: The set of atoms from ``src_grp`` that in fact participate in the interaction. If a sequence of atoms is not provided during the initialization of this class, then all atoms from ``src_grp`` are returned.""" return self._src_interacting_atms or self.src_grp.atoms @property def trgt_interacting_atms(self): """iterable of :class:`~luna.mol.atom.ExtendedAtom`: The set of atoms from ``trgt_grp`` that in fact participate in the interaction. If a sequence of atoms is not provided during the initialization of this class, then all atoms from ``trgt_grp`` are returned.""" return self._trgt_interacting_atms or self.trgt_grp.atoms @property def src_centroid(self): """array_like of float (size 3): Atomic coordinates (x, y, z) of the centroid of ``src_grp``. If it is not provided during the initialization of this class, then it will be calculated automatically from ``src_grp`` or ``src_interacting_atms``. """ if self._src_centroid is None: if self._src_interacting_atms: self._src_centroid = centroid(atom_coordinates(self._src_interacting_atms)) else: src_centroid = self._src_grp.centroid # Define the centroid in a nucleophile with two atoms as the position of its more electronegative atom. # Remember that the position in the interaction object matters. We have defined that the first group is always # the nucleophile for both dipole-dipole and ion-dipole interactions. if self.type in NUCLEOPHILE_INTERS and len(self.src_grp.atoms) == 2: dipole_atm = self.src_grp.atoms[0] if (self.src_grp.atoms[0].electronegativity > self.src_grp.atoms[1].electronegativity) else self.src_grp.atoms[1] src_centroid = dipole_atm.coord # For unfavorable multipolar interactions, it may happen that the first atom group is an electrophile as well. elif self.type == "Unfavorable electrophile-electrophile" and len(self.src_grp.atoms) == 2: dipole_atm = self.src_grp.atoms[0] if (self.src_grp.atoms[0].electronegativity < self.src_grp.atoms[1].electronegativity) else self.src_grp.atoms[1] src_centroid = dipole_atm.coord self._src_centroid = src_centroid return self._src_centroid @src_centroid.setter def src_centroid(self, centroid): if centroid is None: self._src_centroid = None else: self._src_centroid = np.array(centroid) @property def trgt_centroid(self): """array_like of float (size 3): Atomic coordinates (x, y, z) of the centroid of ``trgt_grp``. If it is not provided during the initialization of this class, then it will be calculated automatically from ``trgt_grp`` or ``trgt_interacting_atms``. """ if self._trgt_centroid is None: if self._trgt_interacting_atms: self._trgt_centroid = centroid(atom_coordinates(self._trgt_interacting_atms)) else: trgt_centroid = self._trgt_grp.centroid # Define the centroid in an electrophile with two atoms as the position of its less electronegative atom. # Remember that the position in the interaction object matters. We have defined that the second group is always # the electrophile for both dipole-dipole and ion-dipole interactions. if self.type in ELECTROPHILE_INTERS and len(self.trgt_grp.atoms) == 2: dipole_atm = self.trgt_grp.atoms[0] if (self.trgt_grp.atoms[0].electronegativity < self.trgt_grp.atoms[1].electronegativity) else self.trgt_grp.atoms[1] trgt_centroid = dipole_atm.coord # For unfavorable multipolar interactions, it may happen that the second atom group is a nucleophile as well. elif self.type == "Unfavorable nucleophile-nucleophile" and len(self.trgt_grp.atoms) == 2: dipole_atm = self.trgt_grp.atoms[0] if (self.trgt_grp.atoms[0].electronegativity > self.trgt_grp.atoms[1].electronegativity) else self.trgt_grp.atoms[1] trgt_centroid = dipole_atm.coord self._trgt_centroid = trgt_centroid return self._trgt_centroid @trgt_centroid.setter def trgt_centroid(self, centroid): if centroid is None: self._trgt_centroid = None else: self._trgt_centroid = np.array(centroid) @property def type(self): """str: The interaction type.""" return self._type @type.setter def type(self, new_type): self._type = new_type # Reset hash. self._hash_cache = None @property def params(self): """dict: Interaction parameters (distances, angles, etc).""" return self._params @property def required_interactions(self): """list of `InteractionType`: If this interaction depends on other interactions, then return them as a list. Currently, by default, only water-bridged hydrogen bonds and salt bridges have a dependency on other interactions. The first, depends on two or more hydrogen bonds, while the second depends on an ionic and a hydrogen bond.""" interactions = [] if "depends_on" in self._params: interactions = self._params["depends_on"] return interactions
[docs] def get_partner(self, comp): """Get the partner atom group that forms this interaction with ``comp``. \ Return None if ``comp`` is neither the ``src_grp`` nor ``trgt_grp``. Parameters ---------- comp : :class:`~luna.mol.groups.AtomGroup` Get the partner of this atom group. Returns ------- : :class:`~luna.mol.groups.AtomGroup` or None """ if comp == self.src_grp: return self.trgt_grp elif comp == self.trgt_grp: return self.src_grp return None
[docs] def is_directional(self): """Indicate if the interaction has a direction as in hydrogen bonds and multipolar interactions. Returns ------- : bool """ return self.directional
[docs] def is_intramol_interaction(self): """Indicate if the interaction involves atoms from the same molecule. Returns ------- : bool """ comps1 = self.src_grp.compounds comps2 = self.trgt_grp.compounds return len(comps1) == 1 and len(comps2) == 1 and comps1 == comps2
[docs] def is_intermol_interaction(self): """Indicate if the interaction involves atoms from different molecules. Returns ------- : bool """ return not self.is_intramol_interaction()
def _show_src_centroid(self): show_centroid = True # Define the centroid in a nucleophile with two atoms as the position of its more electronegative atom. # Remember that the position in the interaction object matters. We have defined that the first group is always # the nucleophile for both dipole-dipole and ion-dipole interactions. if self.type in NUCLEOPHILE_INTERS and len(self.src_grp.atoms) == 2: show_centroid = False # For unfavorable multipolar interactions, it may happen that the first atom group is an electrophile as well. elif self.type == "Unfavorable electrophile-electrophile" and len(self.src_grp.atoms) == 2: show_centroid = False return show_centroid def _show_trgt_centroid(self): show_centroid = True # Define the centroid in an electrophile with two atoms as the position of its less electronegative atom. # Remember that the position in the interaction object matters. We have defined that the second group is always # the electrophile for both dipole-dipole and ion-dipole interactions. if self.type in ELECTROPHILE_INTERS and len(self.trgt_grp.atoms) == 2: show_centroid = False # For unfavorable multipolar interactions, it may happen that the second atom group is a nucleophile as well. elif self.type == "Unfavorable nucleophile-nucleophile" and len(self.trgt_grp.atoms) == 2: show_centroid = False return show_centroid def _apply_refs(self): self.src_grp.add_interactions([self]) self.trgt_grp.add_interactions([self])
[docs] def clear_refs(self): """References to this `InteractionType` instance will be removed from the list of interactions of ``src_grp`` and ``trgt_grp``.""" self.src_grp.remove_interactions([self]) self.trgt_grp.remove_interactions([self])
def _expand_dict(self): for key in self._params: self.__dict__[key] = self._params[key]
[docs] def as_json(self): """Represent this interaction as a dict containing the interaction type, flags indicating if its directional or not and if it is an intra- or intermolecular interaction, its default color for visual representations, and information related to each involved atom group. The dict is defined as follows: * ``type`` (str): the interaction type; * ``is_directional`` (bool`): if it is a directional interaction; * ``is_intramol_interaction`` (bool`): if it is an intramolecular interaction; * ``is_intermol_interaction`` (bool`): if it is an intermolecular interaction; * ``color`` (str): the default interaction hex color for visual representations; * ``src_grp`` (dict): information related to the interaction’s first atom or atom group. The dict structure is defined in :meth:`luna.mol.groups.AtomGroup.as_json`. Additional keys: * ``add_pseudo_group`` (bool): if True, it means a pseudo-atom (centroid) should represent ``src_grp`` as it comprises multiple atoms. * ``centroid`` (list of float): the atomic coordinates (x, y, z) of the centroid of ``src_grp`` * ``show_centroid`` (str): if the centroid should be shown or not. It will be False only for nucleophiles and electrophiles. * ``trgt_grp`` (dict): information related to the interaction’s second atom or atom group. The dict structure is defined in :meth:`luna.mol.groups.AtomGroup.as_json`. Additional keys: * ``add_pseudo_group`` (bool): if True, it means a pseudo-atom (centroid) should represent ``trgt_grp`` as it comprises multiple atoms. * ``centroid`` (list of float): the atomic coordinates (x, y, z) of the centroid of ``trgt_grp`` * ``show_centroid`` (str): if the centroid should be shown or not. It will be False only for nucleophiles and electrophiles. """ inter_obj = {} inter_obj["type"] = self.type inter_obj["is_directional"] = self.is_directional() inter_obj["is_intramol_interaction"] = self.is_intramol_interaction() inter_obj["is_intermol_interaction"] = self.is_intermol_interaction() inter_obj["color"] = rgb2hex(*PYMOL_INTERACTION_COLOR_AS_RGB.get_unnormalized_color(self.type)) src_grp_obj = self.src_grp.as_json() src_grp_obj["add_pseudo_group"] = len(self.src_grp.atoms) > 1 src_grp_obj["centroid"] = self.src_centroid.tolist() src_grp_obj["show_centroid"] = self._show_src_centroid() if src_grp_obj["add_pseudo_group"]: src_grp_obj["pseudo_group_name"] = "+".join([a.name for a in sorted(self.src_grp.atoms)]) inter_obj["src_grp"] = src_grp_obj # # Target atom group # trgt_grp_obj = self.trgt_grp.as_json() trgt_grp_obj["add_pseudo_group"] = len(self.trgt_grp.atoms) > 1 trgt_grp_obj["centroid"] = self.trgt_centroid.tolist() trgt_grp_obj["show_centroid"] = self._show_trgt_centroid() if trgt_grp_obj["add_pseudo_group"]: trgt_grp_obj["pseudo_group_name"] = "+".join([a.name for a in sorted(self.trgt_grp.atoms)]) inter_obj["trgt_grp"] = trgt_grp_obj return inter_obj
def __eq__(self, other): """Overrides the default implementation""" if isinstance(self, other.__class__): is_equal_compounds = ((self.src_grp == other.src_grp and self.trgt_grp == other.trgt_grp) or (self.src_grp == other.trgt_grp and self.trgt_grp == other.src_grp)) is_equal_interactions = self.type == other.type has_equal_params = self.params == other.params return is_equal_compounds and is_equal_interactions and has_equal_params return False def __ne__(self, other): """Overrides the default implementation""" return not self.__eq__(other) def __hash__(self): """Overrides the default implementation""" if self._hash_cache is None: # First, it flats the dictionary by transforming it to a list. # Then, it transforms the list into an immutable data structure (tuple). params_values = [] for key in sorted(self.params): if type(self.params[key]) is list: val = tuple(self.params[key]) else: val = self.params[key] params_values.append(val) params_as_tuple = tuple(params_values) # The properties src_grp and trgt_grp properties makes an InteractionType object order dependent. # For example, Class(X,Y) would be considered different from Class(Y,X). # However, in both cases the interactions should be considered the same. # Then, the next line turns the order dependent arguments into an independent order data. comp_values_as_tuple = tuple(sorted([self.src_grp, self.trgt_grp])) self._hash_cache = hash(tuple([comp_values_as_tuple, self.type, params_as_tuple])) return self._hash_cache def __repr__(self): return ('<InteractionType: compounds=(%s, %s) type=%s>' % (self.src_grp, self.trgt_grp, self.type))