#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# This file is part of the `pypath` python module
#
# Copyright 2014-2023
# EMBL, EMBL-EBI, Uniklinik RWTH Aachen, Heidelberg University
#
# Authors: see the file `README.rst`
# Contact: Dénes Türei (turei.denes@gmail.com)
#
# Distributed under the GPLv3 License.
# See accompanying file LICENSE.txt or copy at
# https://www.gnu.org/licenses/gpl-3.0.html
#
# Website: https://pypath.omnipathdb.org/
#
"""
Here we define one class, the :py:class:`Interaction`` which provides a
rich API for representing and querying molecular interactions. The
interactions serve as the building elements of the network and the
:py:class:`pypath.network.Network` object largely relies on methods
of the :py:class:`Interaction`` objects.
"""
from __future__ import annotations
from future.utils import iteritems
from typing import Literal
import importlib as imp
import collections
import operator
import itertools
import functools
import json
import pypath.core.evidence as pypath_evidence
import pypath.internals.resource as pypath_resource
import pypath.share.session as session_mod
import pypath.share.common as common
import pypath.utils.mapping as mapping
import pypath.core.entity as entity
import pypath.core.attrs as attrs_mod
import pypath.utils.orthology as orthology
_logger = session_mod.Logger(name = 'interaction')
_log = _logger._log
InteractionKey = collections.namedtuple(
'InteractionKey',
[
'entity_a',
'entity_b',
],
)
InteractionDataFrameRecord = collections.namedtuple(
'InteractionDataFrameRecord',
[
'id_a',
'id_b',
'type_a',
'type_b',
'directed',
'effect',
'type',
'dmodel',
'sources',
'references',
],
)
InteractionDataFrameRecord.__new__.__defaults__ = (None,) * 8
[docs]
class Interaction(attrs_mod.AttributeHandler):
"""
Represents a unique pair of molecular entities interacting with each
other. One :py:class:`Interaction` object might represent multiple
interactions i.e. with different direction or effect or type (e.g.
transcriptional regulation and post-translational regulation),
each supported by different evidences.
:arg str,pypath.entity.Entity a,b:
The two interacting partners. If an :py:class:`pypath.entity.Entity`
objects provided the other attributes (entity_type, id_type, taxon)
will be ignored.
:arg str id_type_a,id_type_b:
The identifier types for partner ``a`` and ``b`` e.g. ``'uniprot'``.
:arg str entity_type_a,entity_type_b:
The types of the molecular entities ``a`` and ``b``
e.g. ``'protein'``.
:arg int taxon_a,taxon_b:
The NCBI Taxonomy Identifiers of partner ``a`` and ``b``
e.g. ``9606`` for human.
:details:
The arguments ``a`` and ``b`` will be assigned to the attribute ``a``
and ``b`` in an alphabetical order, hence it's possible that
argument ``a`` becomes attribute ``b``.
"""
__slots__ = [
'a',
'b',
'a_b',
'b_a',
'nodes',
'key',
'evidences',
'direction',
'positive',
'negative',
'unknown_effect',
]
_get_methods = {
'datasets',
'entities',
'evidences',
'references',
'curation_effort',
'resource_names',
'resources',
'data_models',
'interaction_types',
'interactions',
'interactions_0',
'interactions_directed',
'interactions_undirected',
'interactions_non_directed',
'interactions_undirected_0',
'interactions_non_directed_0',
'interactions_signed',
'interactions_positive',
'interactions_negative',
'interactions_mutual',
'resources_via',
'resource_names_via',
}
_get_methods_autogen = (
'references',
'resources',
'resources_via',
'resource_names',
'resource_names_via',
'data_models',
'interaction_types',
'datasets',
)
_by_methods = (
'resource',
'reference',
'data_model',
'interaction_type',
'interaction_type_and_data_model',
'interaction_type_and_data_model_and_resource',
)
_count_methods = {
'references',
'resources',
'resources_via',
'resource_names_via',
'resource_names',
'curation_effort',
'entities',
'proteins',
'complexes',
'mirnas',
'interactions',
'interactions_0',
'interactions_directed',
'interactions_signed',
'interactions_positive',
'interactions_negative',
'data_models',
'interaction_types',
}
_get_method_signature = [
('direction', None),
('effect', None),
('resources', None),
('data_model', None),
('interaction_type', None),
('via', None),
('references', None),
]
_degree_modes = (
'ALL',
'IN',
'OUT',
)
_degree_directions = {
'undirected': (None, None),
'non_directed': (False, None),
'directed': (True, None),
'signed': (True, True),
'positive': (True, 'positive'),
'negative': (True, 'negative'),
}
_entity_types = {
'protein',
('complex', 'complexes'),
'mirna',
'lncrna',
# ugly :(
(('small_molecule', 'drug', 'metabolite'),),
None,
}
_entity_values = {
'identifiers',
'labels',
None,
}
[docs]
def __init__(
self,
a,
b,
id_type_a = 'uniprot',
id_type_b = 'uniprot',
entity_type_a = 'protein',
entity_type_b = 'protein',
taxon_a = 9606,
taxon_b = 9606,
attrs = None,
):
a = self._get_entity(
identifier = a,
id_type = id_type_a,
entity_type = entity_type_a,
taxon = taxon_a,
)
b = self._get_entity(
identifier = b,
id_type = id_type_b,
entity_type = entity_type_b,
taxon = taxon_b,
)
self.nodes = tuple(sorted((a, b)))
self.a = self.nodes[0]
self.b = self.nodes[1]
self.key = self._key
self.a_b = (self.nodes[0], self.nodes[1])
self.b_a = (self.nodes[1], self.nodes[0])
self.evidences = pypath_evidence.Evidences()
self.direction = {
self.a_b: pypath_evidence.Evidences(),
self.b_a: pypath_evidence.Evidences(),
'undirected': pypath_evidence.Evidences(),
}
self.positive = {
self.a_b: pypath_evidence.Evidences(),
self.b_a: pypath_evidence.Evidences(),
}
self.negative = {
self.a_b: pypath_evidence.Evidences(),
self.b_a: pypath_evidence.Evidences(),
}
self.unknown_effect = {
self.a_b: pypath_evidence.Evidences(),
self.b_a: pypath_evidence.Evidences(),
}
attrs_mod.AttributeHandler.__init__(self, attrs)
[docs]
def reload(self):
"""
Reloads the object from the module level.
"""
modname = self.__class__.__module__
evmodname = self.evidences.__class__.__module__
enmodname = self.a.__class__.__module__
mod = __import__(modname, fromlist = [modname.split('.')[0]])
evmod = __import__(evmodname, fromlist = [evmodname.split('.')[0]])
enmod = __import__(enmodname, fromlist = [enmodname.split('.')[0]])
imp.reload(mod)
imp.reload(evmod)
imp.reload(enmod)
new = getattr(mod, self.__class__.__name__)
evsnew = getattr(evmod, 'Evidences')
evnew = getattr(evmod, 'Evidence')
ennew = getattr(enmod, 'Entity')
setattr(self, '__class__', new)
for evs in itertools.chain(
(self.evidences,),
self.direction.values(),
self.positive.values(),
self.negative.values(),
self.unknown_effect.values(),
):
evs.__class__ = evsnew
for ev in evs:
ev.__class__ = evnew
self.a.__class__ = ennew
self.b.__class__ = ennew
self._generate_get_methods()
self._generate_count_methods()
self._generate_by_methods()
def _get_entity(
self,
identifier,
id_type = 'uniprot',
entity_type = 'protein',
taxon = 9606,
):
if not isinstance(identifier, entity.Entity):
identifier = entity.Entity(
identifier = identifier,
id_type = id_type,
entity_type = entity_type,
taxon = taxon,
)
return identifier
def _check_nodes_key(self, nodes):
"""Checks if *nodes* is contained in the edge.
:arg list nodes:
Or [tuple], contains the names of the nodes to be checked.
:return:
(*bool*) -- ``True`` if all elements in *nodes* are
contained in the object :py:attr:`nodes` list.
"""
return nodes == self.a_b or nodes == self.b_a
def _check_direction_key(self, direction):
"""
Checks if *direction* is ``'undirected'`` or contains the nodes of
the current edge. Used internally to check that *di* is a valid
key for the object attributes declared on dictionaries.
:arg tuple di:
Or [str], key to be tested for validity.
:return:
(*bool*) -- ``True`` if *di* is ``'undirected'`` or a tuple
of node names contained in the edge, ``False`` otherwise.
"""
return (
direction == 'undirected' or (
isinstance(direction, tuple) and
self._check_nodes_key(direction)
)
)
def id_to_entity(self, identifier):
return (
self.a
if self.a == identifier else
self.b
if self.b == identifier else
None
)
[docs]
def direction_key(self, direction):
"""
The direction keys are tuples of `Entity` objects; this method
creates these tuples from a tuple of strings. The two strings
can be labels or identifiers.
"""
if direction == 'undirected':
return direction
direction = tuple(map(self.id_to_entity, direction))
return (
direction
if direction == self.a_b or direction == self.b_a else
None
)
@staticmethod
def direction_key_identifiers(direction):
if direction == 'undirected':
return direction
return tuple(ent.identifier for ent in direction)
@staticmethod
def _directed_key(direction):
return direction is not None and direction != 'undirected'
[docs]
def add_evidence(
self,
evidence,
direction = 'undirected',
effect = None,
references = None,
attrs = None,
):
"""
Adds directionality information with the corresponding data
source named. Modifies self attributes :py:attr:`dirs` and
:py:attr:`sources`.
:arg resource.NetworkResource,evidence.Evidence evidence:
Either a ``pypath.evidence.Evidence`` object or a resource as
``pypath.resource.NetworkResource`` object. In the latter case
the references can be provided in a separate argument.
:arg tuple direction:
Or [str], the directionality key for which the value on
:py:attr:`dirs` has to be set ``True``.
:arg int effect:
The causal effect of the interaction. 1 or 'stimulation'
corresponds to a stimulatory, -1 or 'inhibition' to an
inhibitory while 0 to an unknown or neutral effect.
:arg set,NoneType references:
A set of references, used only if the resource have been provided
as ``NetworkResource`` object.
:arg dict attrs:
Custom (resource specific) attributes.
"""
direction = self.direction_key(direction)
if direction is None:
_log(
'Attempting to add evidence with non matching '
'interaction partners.'
)
return
evidence = (
evidence
if isinstance(
evidence,
(
pypath_evidence.Evidence,
pypath_evidence.Evidences,
)
) else
pypath_evidence.Evidence(
resource = evidence,
references = references,
)
)
if hasattr(evidence, 'update_attrs'):
evidence.update_attrs(attrs)
self.evidences += evidence
self.direction[direction] += evidence
if direction != 'undirected':
if effect in {1, 'positive', 'stimulation'}:
self.positive[direction] += evidence
elif effect in {-1, 'negative', 'inhibition'}:
self.negative[direction] += evidence
elif effect in {0, 'unknown'}:
self.unknown_effect[direction] += evidence
def __hash__(self):
return hash(self.key)
def __eq__(self, other):
return hasattr(other, 'key') and self.key == other.key
@property
def _key(self):
return InteractionKey(
self.a.key,
self.b.key,
)
def __iadd__(self, other):
if self != other:
_log(
'Attempt to merge interactions with '
'non matching interaction partners.'
)
return self
self._merge_evidences(self, other)
attrs_mod.AttributeHandler.__iadd__(self, other)
return self
def __add__(self, other):
new = self.__copy__()
new += other
return new
def __copy__(self):
new = Interaction(*self.key)
new += self
new.update_attrs(self)
return new
@staticmethod
def _merge_evidences(one, other):
one.evidences += other.evidences
for dir_key in one.direction.keys():
one.direction[dir_key] += other.direction[dir_key]
for eff_key in one.positive.keys():
one.positive[eff_key] += other.positive[eff_key]
for eff_key in one.negative.keys():
one.negative[eff_key] += other.negative[eff_key]
for eff_key in one.unknown_effect.keys():
one.unknown_effect[eff_key] += other.unknown_effect[eff_key]
def __repr__(self):
return '<Interaction: %s [%s]>' % (
self.__str__(),
self.evidences.__repr__().strip('<>'),
)
def __str__(self):
return '%s %s=%s=%s=%s %s' % (
self.a.label or self.a.identifier,
'<' if self.direction[self.b_a] else '=',
(
'(+-)' if (
self.positive[self.b_a] and
self.negative[self.b_a]
) else
'(+)=' if self.positive[self.b_a] else
'(-)=' if self.negative[self.b_a] else
'===='
),
(
'(+-)' if (
self.positive[self.a_b] and
self.negative[self.a_b]
) else
'(+)=' if self.positive[self.a_b] else
'(-)=' if self.negative[self.a_b] else
'===='
),
'>' if self.direction[self.a_b] else '=',
self.b.label or self.b.identifier,
)
def __contains__(self, other):
return (
other == self.a or
other == self.b or
self.evidences.__contains__(other) or
attrs_mod.AttributeHandler.__contains__(self, other)
)
def has_data_model(self, data_model):
return self.evidences.has_data_model(data_model)
@property
def data_models(self):
return {
ev.resource.data_model
for ev in self.evidences
}
def has_dataset(
self,
dataset: str,
direction: str | tuple = None,
effect: Literal['positive', 'negative'] = None,
**kwargs,
) -> bool:
return (
self.get_evidences(direction = direction, effect = effect).
has_dataset(dataset, **kwargs)
)
[docs]
def get_direction(
self,
direction,
resources = False,
evidences = False,
sources = False,
resource_names = False,
):
"""
Returns the state (or *resources* if specified) of the given
*direction*.
:arg tuple direction:
Or [str] (if ``'undirected'``). Pair of nodes from which
direction information is to be retrieved.
:arg bool resources:
Optional, ``'False'`` by default. Specifies if the
:py:attr:`resources` information of the given direction is to
be retrieved instead.
:return:
(*bool* or *set*) -- (if ``resources=True``). Presence/absence
of the requested direction (or the list of resources if
specified). Returns ``None`` if *direction* is not valid.
"""
direction = self.direction_key(direction)
if direction is not None:
return self._select_answer_type(
self.direction[direction],
resources = resources,
evidences = evidences,
resource_names = resource_names,
sources = sources,
)
else:
return None
[docs]
def get_directions(
self,
src,
tgt,
resources = False,
evidences = False,
resource_names = False,
sources = False,
):
"""
Returns all directions with boolean values or list of sources.
:arg str src:
Source node.
:arg str tgt:
Target node.
:arg bool resources:
Optional, ``False`` by default. Specifies whether to return
the :py:attr:`resources` attribute instead of :py:attr:`dirs`.
:return:
Contains the :py:attr:`dirs` (or :py:attr:`resources` if
specified) of the given edge.
"""
query = (src, tgt)
answer_type_args = {
'resources': resources,
'evidences': evidences,
'resource_names': resource_names,
'sources': sources,
}
query = self.direction_key(query)
if query is not None:
return [
self._select_answer_type(
self.direction[query],
**answer_type_args
),
self._select_answer_type(
self.direction[tuple(reversed(query))],
**answer_type_args
),
self._select_answer_type(
self.direction['undirected'],
**answer_type_args
),
]
else:
return None
def _select_answer_type(
self,
answer,
resources = False,
evidences = False,
resource_names = False,
sources = False,
):
return (
answer
if evidences else
answer.get_resources()
if resources else
answer.get_resource_names()
if sources or resource_names else
bool(answer)
)
[docs]
def which_directions(
self,
resources = None,
effect = None,
):
"""
Returns the pair(s) of nodes for which there is information
about their directionality.
:arg str effect:
Either *positive* or *negative*.
:arg str,set resources:
Limits the query to one or more resources. Optional.
:return:
(*tuple*) -- Tuple of tuples with pairs of nodes where the
first element is the source and the second is the target
entity, according to the given resources and limited to the
effect.
"""
resources = self._resources_set(resources)
effect = self._effect_synonyms(effect)
return tuple(
_dir
for _dir, _evidences in iteritems(self.direction)
if _dir != 'undirected' and
_evidences and (
not resources or
_evidences & resources
) and (
not effect
or (
not resources and
getattr(self, effect)[_dir]
) or
getattr(self, effect)[_dir] & resources
)
)
# synonym: old name
which_dirs = which_directions
[docs]
def which_signs(self, resources = None, effect = None):
"""
Returns the pair(s) of nodes for which there is information
about their effect signs.
:param str,set resources:
Limits the query to one or more resources. Optional.
:param str effect:
Either *positive* or *negative*, limiting the query to positive
or negative effects; for any other values effects of both
signs will be returned.
:return:
(*tuple*) -- Tuple of tuples with pairs of nodes where the
first element is a tuple of the source and the target entity,
while the second element is the effect sign, according to
the given resources. E.g. ((('A', 'B'), 'positive'),)
"""
resources = self._resources_set(resources)
effect = self._effect_synonyms(effect)
effects = (effect,) if effect else ('positive', 'negative')
return tuple(
(_dir, _effect)
for _effect in effects
for _dir, _evidences in iteritems(getattr(self, _effect))
if _evidences and (
not resources or
_evidences & resources
)
)
@staticmethod
def _effect_synonyms(effect):
if not effect or effect == True:
return effect
if effect in {'positive', 'stimulation', 'stimulatory'}:
return 'positive'
if effect in {'negative', 'inhibition', 'inhibitory'}:
return 'negative'
if effect in {'unknown'}:
return 'unknown'
def _resources_set(self, resources = None):
return common.to_set(resources)
[docs]
def unset_direction(
self,
direction,
only_sign = False,
resource = None,
interaction_type = None,
via = False,
source = None,
):
"""
Removes directionality and/or source information of the
specified *direction*. Modifies attribute :py:attr:`dirs` and
:py:attr:`sources`.
:arg tuple direction:
Or [str] (if ``'undirected'``) the pair of nodes specifying
the directionality from which the information is to be
removed.
:arg set resource:
Optional, ``None`` by default. If specified, determines
which specific source(s) is(are) to be removed from
:py:attr:`sources` attribute in the specified *direction*.
"""
direction = self.direction_key(direction)
if direction is not None:
attrs = (
(self._effect_synonyms(only_sign),)
if only_sign else
('direction', 'positive', 'negative', 'unknown_effect')
)
resource = resource or source
for attr in attrs:
if resource is not None:
getattr(self, attr)[direction].remove(
resource = resource,
interaction_type = interaction_type,
via = via,
)
else:
getattr(self, attr)[direction] = (
pypath_evidence.Evidences()
)
# synonym: old name
unset_dir = unset_direction
[docs]
def unset_sign(
self,
direction,
sign,
resource = None,
interaction_type = None,
via = False,
source = None,
):
"""
Removes sign and/or source information of the specified
*direction* and *sign*. Modifies attribute :py:attr:`positive`
and :py:attr:`positive_sources` or :py:attr:`negative` and
:py:attr:`negative_sources` (or
:py:attr:`positive_attributes`/:py:attr:`negative_sources`
only if ``source=True``).
:arg tuple direction:
The pair of nodes specifying the directionality from which
the information is to be removed.
:arg str sign:
Sign from which the information is to be removed. Must be
either ``'positive'`` or ``'negative'``.
:arg set source:
Optional, ``None`` by default. If specified, determines
which source(s) is(are) to be removed from the sources in
the specified *direction* and *sign*.
"""
self.unset_direction(
direction = direction,
only_sign = sign,
resource = resource,
interaction_type = interaction_type,
via = via,
source = source,
)
[docs]
def unset_interaction_type(self, interaction_type):
"""
Removes all evidences with a certain ``interaction_type``.
"""
for ev in tuple(self.evidences):
if ev.resource.interaction_type == interaction_type:
self.evidences -= ev
for attr in ('direction', 'positive', 'negative'):
for key, evs in getattr(self, attr):
for ev in tuple(evs):
if ev.resource.interaction_type == interaction_type:
evs -= ev
[docs]
def is_directed(self):
"""
Checks if edge has any directionality information.
:return:
(*bool*) -- Returns ``True`` if any of the :py:attr:`dirs`
attribute values is ``True`` (except ``'undirected'``),
``False`` otherwise.
"""
return any(
evs
for dkey, evs in iteritems(self.direction)
if dkey != 'undirected'
)
[docs]
def is_directed_by_resources(self, resources = None):
"""
Checks if edge has any directionality information from some
resource(s).
:return:
(*bool*) -- Returns ``True`` if any of the :py:attr:`dirs`
attribute values is ``True`` (except ``'undirected'``),
``False`` otherwise.
"""
return self._dir_by_resource(resources, op = operator.or_)
def is_mutual(self, resources = None):
"""
Checks if the edge has mutual directions (both A-->B and B-->A).
"""
return (
bool(self.direction[self.a_b]) and bool(self.direction[self.b_a])
if not resources else
self.is_mutual_by_resources(resources = resources)
)
[docs]
def is_mutual_by_resources(self, resources = None):
"""
Checks if the edge has mutual directions (both A-->B and B-->A)
according to some resource(s).
"""
return self._dir_by_resource(resources, op = operator.and_)
[docs]
def is_loop(self):
"""
:returns:
``True`` if the interaction is a loop edge i.e. its endpoints are the
same node.
"""
return self.a == self.b
def _dir_by_resource(self, resources = None, op = operator.or_):
resources = self._resources_set(resources)
return op(
self.direction[self.a_b] & resources,
self.direction[self.b_a] & resources,
)
[docs]
def is_stimulation(self, direction = None, resources = None):
"""
Checks if any (or for a specific *direction*) interaction is
activation (positive interaction).
:arg tuple direction:
Optional, ``None`` by default. If specified, checks the
:py:attr:`positive` attribute of that specific
directionality. If not specified, checks both.
:return:
(*bool*) -- ``True`` if any interaction (or the specified
*direction*) is activatory (positive).
"""
return self._is_effect(
sign = 'positive',
direction = direction,
resources = resources,
)
[docs]
def is_inhibition(self, direction = None, resources = None):
"""
Checks if any (or for a specific *direction*) interaction is
inhibition (negative interaction).
:arg tuple direction:
Optional, ``None`` by default. If specified, checks the
:py:attr:`negative` attribute of that specific
directionality. If not specified, checks both.
:return:
(*bool*) -- ``True`` if any interaction (or the specified
*direction*) is inhibitory (negative).
"""
return self._is_effect(
sign = 'negative',
direction = direction,
resources = resources,
)
def _is_effect(self, sign, direction = None, resources = None):
_sign = getattr(self, sign)
_resources = self._resources_set(resources)
return (
any(
bool(
_evidences
if not _resources else
_evidences & _resources
)
for _direction, _evidences in iteritems(_sign)
if not direction or direction == _direction
)
)
[docs]
def has_sign(self, direction = None, resources = None):
"""
Checks whether the edge (or for a specific *direction*) has
any signed information (about positive/negative interactions).
:arg tuple direction:
Optional, ``None`` by default. If specified, only the
information of that direction is checked for sign.
:return:
(*bool*) -- ``True`` if there exist any information on the
sign of the interaction, ``False`` otherwise.
"""
return (
self.is_stimulation(direction = direction, resources = resources)
or
self.is_inhibition(direction = direction, resources = resources)
)
[docs]
def add_sign(
self,
direction: tuple,
sign: str,
resource: str | set[str] | None = None,
resource_name: str = None,
interaction_type: str = 'PPI',
data_model: str = None,
attrs: dict = None,
**kwargs
):
"""
Sets sign and source information on a given direction of the
edge. Modifies the attributes :py:attr:`positive` and
:py:attr:`positive_sources` or :py:attr:`negative` and
:py:attr:`negative_sources` depending on the sign. Direction is
also updated accordingly, which also modifies the attributes
:py:attr:`dirs` and :py:attr:`sources`.
Args
direction:
Pair of edge nodes specifying the direction from which the
information is to be set/updated.
sign:
Specifies the type of interaction. Either ``'positive'`` or
``'negative'``.
resource:
Contains the name(s) of the source(s) from which the
information was obtained.
attrs:
Custom (resource specific) edge attributes.
kwargs:
Passed to ``pypath.resource.NetworkResource`` if ``resource``
is not already a ``NetworkResource`` or ``Evidence``
instance.
"""
sign = self._effect_synonyms(sign)
evidence = (
resource
if isinstance(resource, pypath_evidence.Evidence) else
pypath_evidence.Evidence(
resource = resource,
references = references,
)
if isinstance(resource, pypath_resource.NetworkResource) else
pypath_evidence.Evidence(
resource = pypath_resource.NetworkResource(
name = resource_name,
interaction_type = interaction_type,
data_model = data_model,
**kwargs
)
)
if resource_name is not None else
None
)
evidence.update_attrs(attrs)
direction = self.direction_key(direction)
if self._directed_key(direction) and evidence is not None:
ev_attr = getattr(self, sign)
ev_attr += evidence
[docs]
def get_sign(
self,
direction: tuple,
sign: str = None,
evidences: bool = False,
resources: bool = False,
resource_names: bool = False,
sources: bool = False,
) -> list[bool | str]:
"""
Retrieves the sign information of the edge in the given
diretion. If specified in *sign*, only that sign's information
will be retrieved. If specified in *sources*, the sources of
that information will be retrieved instead.
Args
direction:
Contains the pair of nodes specifying the directionality of
the edge from which th information is to be retrieved.
sign:
Optional, ``None`` by default. Denotes whether to retrieve
the ``'positive'`` or ``'negative'`` specific information.
resources:
Optional, ``False`` by default. Specifies whether to return
the resources instead of sign.
Returns
If ``sign=None`` containing [bool] values
denoting the presence of positive and negative sign on that
direction, if ``sources=True`` the [set] of sources for each
of them will be returned instead. If *sign* is specified,
returns [bool] or [set] (if ``sources=True``) of that
specific direction and sign.
"""
sign = self._effect_synonyms(sign)
answer_type_args = {
'resources': resources,
'evidences': evidences,
'resource_names': resource_names,
'sources': sources,
}
direction = self.direction_key(direction)
if self._directed_key(direction):
return (
self._select_answer_type(
getattr(self, sign)[direction],
**answer_type_args
)
if sign else
[
self._select_answer_type(
self.positive[direction],
**answer_type_args
),
self._select_answer_type(
self.negative[direction],
**answer_type_args
)
]
)
[docs]
def source(
self,
undirected: bool = False,
resources = None,
**kwargs
):
"""
Returns the name(s) of the source node(s) for each existing
direction on the interaction.
Args
undirected:
Optional, ``False`` by default.
Returns
(*list*) -- Contains the name(s) for the source node(s).
This means if the interaction is bidirectional, the list
will contain both identifiers on the edge. If the
interaction is undirected, an empty list will be returned.
"""
return self._partner(
source_target = 'source',
undirected = undirected,
resources = resources,
**kwargs
)
# synonym: old name
src = source
[docs]
def target(
self,
undirected: bool = False,
resources = None,
**kwargs
):
"""
Returns the name(s) of the target node(s) for each existing
direction on the interaction.
Args
undirected:
Optional, ``False`` by default.
Returns
(*list*) -- Contains the name(s) for the target node(s).
This means if the interaction is bidirectional, the list
will contain both identifiers on the edge. If the
interaction is undirected, an empty list will be returned.
"""
return self._partner(
source_target = 'target',
undirected = undirected,
resources = resources,
**kwargs
)
# synonym: old name
tgt = target
def _partner(
self,
source_target,
undirected = False,
resources = None,
**kwargs
):
resources = self._resources_set(resources)
_slice = slice(0, 1) if source_target == 'source' else slice(1, 2)
return tuple(itertools.chain(
(
_direction[_slice]
if _direction != 'undirected' else
self.nodes
if undirected else
()
)
for _direction, _evidences in iteritems(self.direction)
if (
(
(
not resources and
not kwargs and
bool(_evidences)
) or
(
any(
ev.match(
resource = res,
**kwargs
)
for res in resources or (None,)
for ev in _evidences
)
)
)
)
))
[docs]
def src_by_resource(self, resource):
"""
Returns the name(s) of the source node(s) for each existing
direction on the interaction for a specific *resource*.
:arg str resource:
Name of the resource according to which the information is to
be retrieved.
:return:
(*list*) -- Contains the name(s) for the source node(s)
according to the specified *resource*. This means if the
interaction is bidirectional, the list will contain both
identifiers on the edge. If the specified *source* is not
found or invalid, an empty list will be returned.
"""
return [
_dir[0]
for _dir, _evidences in iteritems(self.direction)
if (
_dir != 'undirected' and
resource in _evidences
)
]
[docs]
def tgt_by_resource(self, resource):
"""
Returns the name(s) of the target node(s) for each existing
direction on the interaction for a specific *resource*.
:arg str resource:
Name of the resource according to which the information is to
be retrieved.
:return:
(*list*) -- Contains the name(s) for the target node(s)
according to the specified *resource*. This means if the
interaction is bidirectional, the list will contain both
identifiers on the edge. If the specified *source* is not
found or invalid, an empty list will be returned.
"""
return [
_dir[1]
for _dir, _evidences in iteritems(self.direction)
if (
_dir != 'undirected' and
resource in _evidences
)
]
[docs]
def resources_a_b(
self,
resources = False,
evidences = False,
resource_names = False,
sources = False,
):
"""
Retrieves the list of resources for the :py:attr:`a_b`
direction.
:return:
(*set*) -- Contains the names of the sources supporting the
:py:attr:`a_b` directionality of the edge.
"""
answer_type_args = {
'resources': resources,
'evidences': evidences,
'resource_names': resource_names,
'sources': sources,
}
return self._select_answer_type(
self.direction[self.a_b],
**answer_type_args
)
# synonym for old method name
sources_straight = resources_a_b
[docs]
def resources_b_a(
self,
resources = False,
evidences = False,
resource_names = False,
sources = False,
):
"""
Retrieves the list of sources for the :py:attr:`b_a` direction.
:return:
(*set*) -- Contains the names of the sources supporting the
:py:attr:`b_a` directionality of the edge.
"""
answer_type_args = {
'resources': resources,
'evidences': evidences,
'resource_names': resource_names,
'sources': sources,
}
return self._select_answer_type(
self.direction[self.b_a],
**answer_type_args
)
# synonym for old method name
sources_reverse = resources_b_a
[docs]
def resources_undirected(
self,
resources = False,
evidences = False,
resource_names = False,
sources = False,
):
"""
Retrieves the list of resources without directed information.
:return:
(*set*) -- Contains the names of the sources supporting the
edge presence but without specific directionality
information.
"""
answer_type_args = {
'resources': resources,
'evidences': evidences,
'resource_names': resource_names,
'sources': sources,
}
return self._select_answer_type(
self.direction['undirected'],
**answer_type_args
)
sources_undirected = resources_undirected
[docs]
def positive_a_b(self):
"""
Checks if the :py:attr:`a_b` directionality is a positive
interaction.
:return:
(*bool*) -- ``True`` if there is supporting information on
the :py:attr:`a_b` direction of the edge as activation.
``False`` otherwise.
"""
return bool(self.positive[self.a_b])
positive_straight = positive_a_b
[docs]
def positive_b_a(self):
"""
Checks if the :py:attr:`b_a` directionality is a positive
interaction.
:return:
(*bool*) -- ``True`` if there is supporting information on
the :py:attr:`b_a` direction of the edge as activation.
``False`` otherwise.
"""
return bool(self.positive[self.b_a])
positive_reverse = positive_b_a
[docs]
def negative_a_b(self):
"""
Checks if the :py:attr:`a_b` directionality is a negative
interaction.
:return:
(*bool*) -- ``True`` if there is supporting information on
the :py:attr:`a_b` direction of the edge as inhibition.
``False`` otherwise.
"""
return bool(self.negative[self.a_b])
negative_straight = negative_a_b
[docs]
def negative_b_a(self):
"""
Checks if the :py:attr:`b_a` directionality is a negative
interaction.
:return:
(*bool*) -- ``True`` if there is supporting information on
the :py:attr:`b_a` direction of the edge as inhibition.
``False`` otherwise.
"""
return bool(self.negative[self.b_a])
negative_reverse = negative_b_a
[docs]
def negative_resources_a_b(self, **kwargs):
"""
Retrieves the list of resources for the :py:attr:`a_b`
direction and negative sign.
:return:
(*set*) -- Contains the names of the resources supporting the
:py:attr:`a_b` directionality of the edge with a
negative sign.
"""
answer_type_args = {
'resource_names': True
}
answer_type_args.update(kwargs)
return self._select_answer_type(
self.negative[self.a_b],
**answer_type_args
)
[docs]
def negative_resources_b_a(self, **kwargs):
"""
Retrieves the list of resources for the :py:attr:`b_a`
direction and negative sign.
:return:
(*set*) -- Contains the names of the resources supporting the
:py:attr:`b_a` directionality of the edge with a
negative sign.
"""
answer_type_args = {
'resource_names': True
}
answer_type_args.update(kwargs)
return self._select_answer_type(
self.negative[self.b_a],
**answer_type_args
)
[docs]
def positive_resources_a_b(self, **kwargs):
"""
Retrieves the list of resources for the :py:attr:`a_b`
direction and positive sign.
:return:
(*set*) -- Contains the names of the resources supporting the
:py:attr:`a_b` directionality of the edge with a
positive sign.
"""
answer_type_args = {
'resource_names': True
}
answer_type_args.update(kwargs)
return self._select_answer_type(
self.positive[self.a_b],
**answer_type_args
)
[docs]
def positive_resources_b_a(self, **kwargs):
"""
Retrieves the list of resources for the :py:attr:`b_a`
direction and positive sign.
:return:
(*set*) -- Contains the names of the resources supporting the
:py:attr:`b_a` directionality of the edge with a
positive sign.
"""
answer_type_args = {
'resource_names': True
}
answer_type_args.update(kwargs)
return self._select_answer_type(
self.positive[self.b_a],
**answer_type_args
)
[docs]
def majority_dir(
self,
only_interaction_type = None,
only_primary = True,
by_references = False,
by_reference_resource_pairs = True,
):
"""
Infers which is the major directionality of the edge by number
of supporting sources.
:return:
(*tuple*) -- Contains the pair of nodes denoting the
consensus directionality. If the number of sources on both
directions is equal, ``None`` is returned. If there is no
directionality information, ``'undirected'``` will be
returned.
"""
a_b = self.direction[self.a_b]
b_a = self.direction[self.b_a]
if not a_b and not b_a:
return 'undirected'
method = (
'count_references'
if by_references else
'count_curation_effort'
if by_reference_resource_pairs else
'count_resources'
)
n_a_b = getattr(a_b, method)(
interaction_type = only_interaction_type,
via = False if only_primary else None,
)
n_b_a = getattr(b_a, method)(
interaction_type = only_interaction_type,
via = False if only_primary else None,
)
return (
'undirected'
if n_a_b == 0 and n_b_a == 0 else
None
if n_a_b == n_b_a else
self.a_b
if n_a_b > n_b_a else
self.b_a
)
[docs]
def majority_sign(
self,
only_interaction_type = None,
only_primary = True,
by_references = False,
by_reference_resource_pairs = True,
):
"""
Infers which is the major sign (activation/inhibition) of the
edge by number of supporting sources on both directions.
:return:
(*dict*) -- Keys are the node tuples on both directions
(:py:attr:`straight`/:py:attr:`reverse`) and values can be
either ``None`` if that direction has no sign information or
a list of two [bool] elements corresponding to majority of
positive and majority of negative support. In case both
elements of the list are ``True``, this means the number of
supporting sources for both signs in that direction is
equal.
"""
result = {}
method = (
'count_references'
if by_references else
'count_curation_effort'
if by_reference_resource_pairs else
'count_resources'
)
for _dir in (self.a_b, self.b_a):
n_pos = getattr(self.positive[_dir], method)(
interaction_type = only_interaction_type,
via = False if only_primary else None,
)
n_neg = getattr(self.negative[_dir], method)(
interaction_type = only_interaction_type,
via = False if only_primary else None,
)
result[_dir] = [
0 < n_pos >= n_neg,
0 < n_neg >= n_pos,
]
return result
[docs]
def consensus(
self,
only_interaction_type = None,
only_primary = False,
by_references = False,
by_reference_resource_pairs = True,
):
"""
Infers the consensus edge(s) according to the number of
supporting sources. This includes direction and sign.
:return:
(*list*) -- Contains the consensus edge(s) along with the
consensus sign. If there is no major directionality, both
are returned. The structure is as follows:
``['<source>', '<target>', '<(un)directed>', '<sign>']``
"""
result = []
_dir = self.majority_dir(
only_interaction_type = only_interaction_type,
only_primary = only_primary,
by_references = by_references,
by_reference_resource_pairs = by_reference_resource_pairs,
)
if _dir == 'undirected' or _dir is None:
_dir = self.majority_dir(
only_interaction_type = only_interaction_type,
only_primary = only_primary,
by_references = False,
by_reference_resource_pairs = False,
)
_effect = self.majority_sign(
only_interaction_type = only_interaction_type,
only_primary = only_primary,
by_references = by_references,
by_reference_resource_pairs = by_reference_resource_pairs,
)
_effect_noref = self.majority_sign(
only_interaction_type = only_interaction_type,
only_primary = only_primary,
by_references = False,
by_reference_resource_pairs = False,
)
if _dir == 'undirected':
result.append([
self.a_b[0],
self.a_b[1],
'undirected',
'unknown',
])
else:
dirs = (self.a_b, self.b_a) if _dir is None else (_dir,)
for d in dirs:
d_effect = (
_effect[d]
if (
_effect[d] is not None and
_effect[d][0] != _effect[d][1]
) else
_effect_noref[d]
)
if d_effect is not None:
# index #0 is positive
if d_effect[0]:
result.append([
d[0],
d[1],
'directed',
'positive',
])
# can not be elif bc of the case of equal weight of
# evidences for both positive and negative
if d_effect[1]:
result.append([
d[0],
d[1],
'directed',
'negative',
])
# directed with unknown effect
else:
result.append([
d[0],
d[1],
'directed',
'unknown',
])
return result
consensus_edges = consensus
[docs]
def merge(self, other):
"""
Merges current Interaction with another (if and only if they are the
same class and contain the same nodes). Updates the attributes
:py:attr:`direction`, :py:attr:`positive` and :py:attr:`negative`.
:arg pypath.interaction.Interaction other:
The new Interaction object to be merged with the current one.
"""
if not self._check_nodes_key(other.nodes):
_log(
'Attempting to merge Interaction instances with different '
'interacting partners.'
)
return
self.evidences += other.evidences
for attr, _dir in itertools.product(
('direction', 'positive', 'negative'),
(self.a_b, self.b_a, 'undirected')
):
if attr != 'direction' and _dir == 'undirected':
continue
getattr(self, attr)[_dir] += getattr(other, attr)[_dir]
[docs]
def translate(self, ids, new_attrs = None):
"""
Translates the node names/identifiers according to the
dictionary *ids*. Also is able to change attributes like `id_type`,
`taxon` and `entity_type`.
:arg dict ids:
Dictionary containing (at least) the current names of the
nodes as keys and their translation as values.
:arg dict new_attrs:
Dictionary with new IDs as keys and their dicts of their new
attributes as values. For any attribute not provided here
the attributes from the original instance will be used.
E.g. you can provide `{'1956': {'id_type': 'entrez'}}' if the
new ID type for protein EGFR is Entrez Gene ID.
:return:
(*pypath.main.Direction*) -- The copy of current edge object
with translated node names.
"""
new_a = ids[self.nodes[0]]
new_b = ids[self.nodes[1]]
new_ids = {'a': new_a, 'b': new_b}
to_old = common.swap_dict_simple(ids)
all_new_attrs = dict(
(
'%s_%s' % (attr, label),
new_attrs[new_ids[label]][attr]
if (
new_ids[label] in new_attrs and
attr in new_attrs[new_ids[label]]
) else
getattr(getattr(self, label), attr)
)
for attr in ('id_type', 'entity_type', 'taxon')
for label in ('a', 'b')
)
all_new_attrs['attrs'] = new_attrs.get('attrs', self.attrs)
new = Interaction(
a = new_a,
b = new_b,
**all_new_attrs
)
new.evidences += self.evidences
to_old = dict(
(
new_id,
self.id_to_entity(old_id)
)
for new_id, old_id in iteritems(to_old)
)
# this is required to handle also loop edges
new_old_a_b = (
(
to_old[new.a.identifier],
to_old[new.b.identifier],
)
if new.a != new.b else
self.a_b
)
new_old_b_a = (
(
to_old[new.b.identifier],
to_old[new.a.identifier],
)
if new.a != new.b else
self.b_a
)
for (old_dir, new_dir), attr in itertools.product(
zip(
(
new_old_a_b,
new_old_b_a,
'undirected'
),
(
new.a_b,
new.b_a,
'undirected',
),
),
('direction', 'positive', 'negative'),
):
if old_dir == 'undirected' and attr != 'direction':
continue
getattr(new, attr)[new_dir] += getattr(self, attr)[old_dir]
return new
def orthology_translate_one(self, id_a, id_b, taxon):
return self.translate(
ids = {
self.a: id_a,
self.b: id_b,
},
new_attrs = {
id_a: {
'taxon': (
self.a.taxon
if id_a == self.a.identifier else
taxon
),
},
id_b: {
'taxon': (
self.b.taxon
if id_b == self.b.identifier else
taxon
),
},
},
)
def orthology_translate(self, taxon, exclude = None):
exclude = exclude or set()
exclude.add(0)
for new_a, new_b in itertools.product(
(self.a.identifier,)
if self.a.taxon in exclude else
orthology.translate(
source_id = self.a.identifier,
target = taxon,
source = self.a.taxon,
),
(self.b.identifier,)
if self.b.taxon in exclude else
orthology.translate(
source_id = self.b.identifier,
target = taxon,
source = self.b.taxon,
),
):
yield self.orthology_translate_one(
id_a = new_a,
id_b = new_b,
taxon = taxon,
)
def get_evidences(
self,
direction = None,
effect = None,
resources = None,
data_model = None,
interaction_type = None,
via = None,
references = None,
datasets = None,
):
effect = self._effect_synonyms(effect)
evidences = (
# any signed
sum(itertools.chain(
self.positive.values(),
self.negative.values(),
))
if effect == True else
# only positive
(
self.positive[direction]
if direction in self.positive else
sum(self.positive.values())
)
if effect == 'positive' else
# only negative
(
self.negative[direction]
if direction in self.negative else
sum(self.negative.values())
)
if effect == 'negative' else
# any directed
sum(self.direction[_dir] for _dir in self.which_dirs())
if direction == True else
# one specific direction
self.direction[direction]
if direction in self.direction else
# all evidences (default)
self.evidences
)
return (
pypath_evidence.Evidences(
evidences.filter(
resource = resources,
interaction_type = interaction_type,
via = via,
data_model = data_model,
references = references,
datasets = datasets,
)
)
)
[docs]
def get_entities(
self,
entity_type = None,
direction = None,
effect = None,
resources = None,
data_model = None,
interaction_type = None,
via = None,
references = None,
datasets = None,
return_type = None,
):
"""
Retrieves the entities involved in interactions matching the criteria.
It either returns both interacting entities in a *set* or an empty
*set*. This may not sound so useful at the level of this object but
becomes more useful once we want to collect entities having certain
kind of interactions across a series of `Interaction` objects.
:arg str entity_type:
The type of the molecular entity. Possible values: `protein`,
`complex`, `mirna`, `small_molecule`.
:arg str return_type:
The type of values to return. Default is
py:class:``pypath.entity.Entity`` objects, alternatives are
``labels`` ``identifiers``.
"""
# TODO: this method could be made slightly more efficient by using
# not ``get_interactions`` but a simpler logic as here we don't need
# to handle the directions separately; however this is not very
# important and for the time being it's good as it is.
kwargs = locals()
_ = kwargs.pop('self')
entity_type = common.to_set(kwargs.pop('entity_type'))
return_type = kwargs.pop('return_type')
return_types = {
'entity': None,
'entities': None,
'id': 'identifier',
'name': 'identifier',
}
# allow plurals
return_type = (
return_type[:-1]
if (
isinstance(return_type, str) and
return_type[-1] == 's'
) else
return_type
)
# allow some synonyms
return_type = (
return_types[return_type]
if return_type in return_types else
return_type
)
return (
set(
(
getattr(en, return_type)
if return_type and hasattr(en, return_type) else
en
)
for en in
itertools.chain(
*self.get_interactions(**kwargs)
)
if not entity_type or en.entity_type in entity_type
)
)
@classmethod
def _generate_entity_methods(cls):
def _create_entity_method(entity_type, return_type):
def _entity_method(*args, **kwargs):
self = args[0]
kwargs['entity_type'] = entity_type
kwargs['return_type'] = return_type
return self.get_entities(*args[1:], **kwargs)
return _entity_method
for etype, vtype in itertools.product(
cls._entity_types,
cls._entity_values,
):
if etype is None and vtype is None:
continue
entity_type = common.sfirst(etype)
entity_type_label = common.sfirst(entity_type)
return_type = vtype
etype_part = (
''
if not entity_type_label else
entity_type_label
if vtype else
etype[1]
if isinstance(etype, tuple) and len(etype) > 1 else
'%ss' % entity_type_label
)
vtype_part = '%s' % vtype if vtype else ''
_method_name = '%s%s%s' % (
etype_part,
'_' if etype_part and vtype_part else '',
vtype_part,
)
method_name = 'get_%s' % _method_name
method = _create_entity_method(
entity_type = entity_type,
return_type = return_type,
)
cls._add_method(
method_name,
method,
signature = (
['self', ('entity_type', None)] +
cls._get_method_signature +
[('return_type', None)]
),
doc = cls.get_entities.__doc__,
)
cls._get_methods.add(_method_name)
cls._count_methods.add(_method_name)
[docs]
def get_interactions(
self,
direction = None,
effect = None,
resources = None,
data_model = None,
interaction_type = None,
via = None,
references = None,
entity_type = None,
source_entity_type = None,
target_entity_type = None,
datasets = None,
):
"""
Returns one or two tuples of the interacting partners: one if only
one direction, two if both directions match the query criteria.
The tuple will be empty if no evidence matches the criteria.
:arg NontType,bool,tuple direction:
If `None` both undirected and directed, if `True` only directed,
if a *tuple* of entities only the interactions with that specific
direction will be considered. Unless you set this parameter to
`True` this method will return both directions if one or more
undirected resources present.
If `False`, only the undirected interactions will be considered,
and if any resource annotates this interaction as undirected
both directions will be returned. However the
``count_interactions_undirected`` method will return `1`
in this case.
:arg NoneType,bool,str effect:
If `None` also interactions without effect, if `True` only
the ones with any effect, if a string naming an effect only the
interactions with that specific effect will be considered.
:arg NontType,str,set resources:
Optionally limit the query to one or more resources.
:arg NontType,str,set data_model:
Optionally limit the query to one or more data models e.g.
`activity_flow`.
:arg NontType,str,set interaction_type:
Optionally limit the query to one or more interaction types
e.g. `PPI`.
:arg NontType,bool,str,set via:
Optionally limit the query to certain secondary databases or
if `False` consider only data from primary databases.
:arg str entity_type:
Molecule type for both of the entities.
:arg str source_entity_type:
Molecule type for the source entity.
:arg str target_entity_type:
Molecule type for the target entity.
"""
effect = self._effect_synonyms(effect)
direction = (
self.direction_key(direction)
if isinstance(direction, tuple) else
direction
)
entity_type = common.to_set(entity_type)
source_entity_type = common.to_set(source_entity_type) or entity_type
target_entity_type = common.to_set(target_entity_type) or entity_type
return tuple(
# direction key
_dir
# possible directions
for _dir in (self.a_b, self.b_a)
# conditions by selecting and evaluating evidence collections
if (
(
not source_entity_type or
_dir[0].entity_type in source_entity_type
) and
(
not target_entity_type or
_dir[1].entity_type in target_entity_type
)
)
and
(
self.evaluate_evidences(
this_direction = _dir,
direction = direction,
effect = effect,
resources = resources,
data_model = data_model,
interaction_type = interaction_type,
via = via,
references = references,
datasets = datasets,
)
)
)
[docs]
def evaluate_evidences(
self,
this_direction,
direction = None,
effect = None,
resources = None,
data_model = None,
interaction_type = None,
via = None,
references = None,
datasets = None,
):
"""
Selects the evidence collections matching the direction and effect
criteria and then evaluates if any of the evidences in these
collections match the evidence criteria.
"""
kwargs = locals()
_ = kwargs.pop('self')
return any(self.iter_match_evidences(**kwargs))
[docs]
def iter_match_evidences(
self,
this_direction,
direction = None,
effect = None,
resources = None,
data_model = None,
interaction_type = None,
via = None,
references = None,
datasets = None,
):
"""
Selects the evidence collections matching the direction and effect
criteria and yields collections matching the evidence criteria.
"""
for evs in self.iter_evidences(
this_direction = this_direction,
direction = direction,
effect = effect,
):
if evs.match(
resource = resources,
data_model = data_model,
interaction_type = interaction_type,
via = via,
references = references,
datasets = datasets,
):
yield evs
[docs]
def iter_evidences(
self,
this_direction,
direction = None,
effect = None,
):
"""
Selects and yields evidence collections matching the direction and
effect criteria.
"""
# evidence keys
for evs_key in ('undirected', this_direction):
# evidence dicts
for this_effect in ('direction', 'positive', 'negative'):
if (
# only undirected
(
direction == False and
evs_key == 'undirected' and
this_effect == 'direction'
) or
# undirected
(
direction is None and
not effect and
this_effect == 'direction'
) or
# directed
(
direction != False and
evs_key != 'undirected' and
this_effect == 'direction' and
not effect and (
# any direction
direction == True or
# specific direction
direction == this_direction
)
) or
# with effect
(
direction != False and
evs_key != 'undirected' and
this_effect != 'direction' and (
# any effect
effect == True or
# specific effect
effect == this_effect
)
)
):
# getting the evidence dict and the key from it
yield getattr(self, this_effect)[evs_key]
[docs]
def get_interactions_0(self, **kwargs):
"""
Returns unique interacting pairs without being aware of the direction.
"""
kwargs['direction'] = None
kwargs['effect'] = None
result = self.get_interactions(**kwargs)
return result[:1] if result else ()
[docs]
def get_interactions_directed(self, **kwargs):
"""
Args
kwargs:
See the docs of method ``get_interactions``.
"""
if 'direction' not in kwargs or kwargs['direction'] is None:
kwargs['direction'] = True
return self.get_interactions(**kwargs)
[docs]
def get_interactions_undirected(self, **kwargs):
"""
Only the undirected interactions will be considered, if any resource
annotates this interaction as undirected both directions will be
returned, no matter if certain resources provide direction. However
the ``count_interactions_undirected`` method will return `1` in this
case.
Args
kwargs:
See the docs of method ``get_interactions``.
"""
kwargs['direction'] = False
return self.get_interactions(**kwargs)
[docs]
def get_interactions_undirected_0(self, **kwargs):
"""
Only the undirected interactions will be considered, if any resource
annotates this interaction as undirected the interacting pair as
a sorted tuple will be returned inside a one element tuple.
Args
kwargs:
See the docs of method ``get_interactions``.
"""
undir = self.get_interactions_undirected(**kwargs)
return undir[:1] if undir else ()
[docs]
def get_interactions_non_directed(self, **kwargs):
"""
Only the undirected interactions will be considered, if any resource
annotates this interaction as undirected both directions will be
returned, but only if no resource provide direction. However
the ``count_interactions_non_directed`` method will return `1` in
this case.
Args
kwargs:
See the docs of method ``get_interactions``.
"""
kwargs['direction'] = True
return (
self.get_interactions_undirected(**kwargs)
if not self.get_interactions(**kwargs) else
()
)
[docs]
def get_interactions_non_directed_0(self, **kwargs):
"""
Only the undirected interactions will be considered, if any resource
annotates this interaction as undirected and none as directed, the
interacting pair as a sorted tuple will be returned inside a one
element tuple.
Args
kwargs:
See the docs of method ``get_interactions``.
"""
nondir = self.get_interactions_non_directed(**kwargs)
return nondir[:1] if nondir else ()
[docs]
def get_interactions_signed(self, **kwargs):
"""
Args
kwargs:
See the docs of method ``get_interactions``.
"""
if 'effect' not in kwargs or kwargs['effect'] is None:
kwargs['effect'] = True
return self.get_interactions(**kwargs)
[docs]
def get_interactions_positive(self, **kwargs):
"""
Args
kwargs:
See the docs of method ``get_interactions``.
"""
kwargs['effect'] = 'positive'
return self.get_interactions(**kwargs)
[docs]
def get_interactions_negative(self, **kwargs):
"""
Args
kwargs:
See the docs of method ``get_interactions``.
"""
kwargs['effect'] = 'negative'
return self.get_interactions(**kwargs)
[docs]
def get_interactions_mutual(self, **kwargs):
"""
Note: undirected interactions does not count as mutual but only
interactions with explicit direction information for both directions.
Args
kwargs:
See the docs of method ``get_interactions``.
"""
if 'direction' not in kwargs or kwargs['direction'] is None:
kwargs['direction'] = True
interactions = self.get_interactions(**kwargs)
return interactions if len(interactions) == 2 else ()
[docs]
def is_mutual(self, **kwargs):
"""
Note: undirected interactions does not count as mutual but only
interactions with explicit direction information for both directions.
Args
kwargs:
See the docs of method ``get_interactions``.
"""
return bool(self.get_interactions_mutual(**kwargs))
[docs]
def count_interactions_mutual(self, **kwargs):
"""
Note: undirected interactions does not count as mutual but only
interactions with explicit direction information for both directions.
Args
kwargs:
See the docs of method ``get_interactions``.
"""
return int(self.is_mutual(**kwargs))
[docs]
def count_interactions_undirected(self, **kwargs):
"""
Returns `True` if any resource annotates this interaction without
direction.
Args
kwargs:
See the docs of method ``get_interactions``.
"""
return bool(self.get_interactions_undirected(**kwargs))
[docs]
def count_interactions_non_directed(self, **kwargs):
"""
Returns `True` if any resource annotates this interaction without
and no resource with direction.
Args
kwargs:
See the docs of method ``get_interactions``.
"""
return bool(self.get_interactions_non_directed(**kwargs))
[docs]
def get_degrees(
self,
mode: str,
direction = None,
effect = None,
resources = None,
data_model = None,
interaction_type = None,
via = None,
references = None,
):
"""
Returns a *set* of nodes with the connections matching the direction,
effect and evidence criteria. E.g. if the query concerns the incoming
degrees with positive effect and the matching evidences show A
activates B, but not the other way around, only "B" will be returned.
Args
mode:
The type of degrees to be considered. Three possible values are
``'IN'``, `'OUT'`` and ``'ALL'`` for incoming, outgoing and all
connections, respectively. If the ``direction`` is ``False``
the only possible mode is ``ALL``. If the ``direction`` is
``None`` and also directed evidence(s) match the criteria these
will overwrite the undirected evidences and only the directed
result will be returned.
"""
kwargs = locals()
_ = kwargs.pop('self')
mode = kwargs.pop('mode')
idx = {
'ALL': (0, 2),
'OUT': (0, 1),
'IN': (1, 2),
}
if direction == False:
mode = 'ALL'
if direction is None and not effect:
_ = kwargs.pop('direction')
return (
self.get_degrees(mode = mode, direction = True, **kwargs) or
self.get_degrees(mode = mode, direction = False, **kwargs)
)
result = set()
node_pairs = self.get_interactions(**kwargs)
for pair in node_pairs:
result.update(
pair[
idx[mode][0]:
idx[mode][1]
]
)
return result
def get_curation_effort(self, **kwargs):
return tuple(
(self.a, self.b, res, ref)
for res, refs in
iteritems(self.references_by_resource(**kwargs))
for ref in refs
)
@staticmethod
def _get(self, method, **kwargs):
via = kwargs['via'] if 'via' in kwargs else False
return getattr(
self.get_evidences(
**kwargs
),
'get_%s' % method,
)(via = via)
@staticmethod
def _count(method):
@functools.wraps(method)
def count_method(*args, **kwargs):
return len(method(*args, **kwargs))
return count_method
@staticmethod
def _by(method, by = 'resources'):
by = (by,) if isinstance(by, str) else by
@functools.wraps(method)
def by_method(*args, **kwargs):
self = args[0]
name_keys = kwargs.pop('name_keys', True)
for _by in by:
_ = kwargs.pop(_by, None)
levels_methods = (
'get_%s%ss' % (
_by[:-1] if _by in {'resources', 'references'} else _by,
'_name' if _by == 'resources' and name_keys else ''
)
for _by in by
)
levels = list(itertools.product(*(
getattr(self, levels_method)(**kwargs)
for levels_method in levels_methods
)))
result = dict(
(
_levels if len(_levels) > 1 else _levels[0],
method(
*args,
**dict(zip(by, _levels)),
**kwargs
)
)
for _levels in levels
)
return dict((k, v) for k, v in iteritems(result) if v)
return by_method
@classmethod
def _by_resource(cls, method):
return cls._by(method, by = 'resources')
@classmethod
def _by_data_model(cls, method):
return cls._by(method, by = 'data_model')
@classmethod
def _by_interaction_type_and_data_model(cls, method):
return cls._by(method, by = ('interaction_type', 'data_model'))
@classmethod
def _by_interaction_type_and_data_model_and_resource(cls, method):
return cls._by(
method,
by = ('interaction_type', 'data_model', 'resources'),
)
@classmethod
def _by_interaction_type(cls, method):
return cls._by(method, by = 'interaction_type')
@classmethod
def _by_reference(cls, method):
return cls._by(method, by = 'references')
@classmethod
def _generate_get_methods(cls):
def _create_get_method(method):
@functools.wraps(method)
def _get_method(*args, **kwargs):
return cls._get(self = args[0], method = method, **kwargs)
return _get_method
for _get in cls._get_methods_autogen:
method_name = 'get_%s' % _get
signature = cls._update_get_method_signature(method_name)
cls._add_method(
method_name = method_name,
method = _create_get_method(_get),
# this is not always correct, to be fixed later
signature = signature,
doc = (
'Retrieves %s matching the criteria.' % (
_get.replace('_', ' ')
)
),
)
@classmethod
def _generate_degree_methods(cls):
def _create_degree_method(mode, direction, effect):
wrap_args = mode, direction, effect
@functools.wraps(wrap_args)
def _degree_method(*args, **kwargs):
mode, direction, effect = wrap_args
kwargs['direction'] = direction
kwargs['effect'] = effect
return cls.get_degrees(self = args[0], mode = mode, **kwargs)
return _degree_method
for mode, (dir_label, dir_args) in itertools.product(
cls._degree_modes,
iteritems(cls._degree_directions)
):
if dir_label in {'undirected', 'non_directed'} and mode != 'ALL':
continue
method_name = 'degrees_%s%s' % (
dir_label,
'_%s' % mode.lower() if mode != 'ALL' else ''
)
cls._count_methods.add(method_name)
cls._get_methods.add(method_name)
method = _create_degree_method(mode, *dir_args)
_method_name = 'get_%s' % method_name
cls._add_method(
method_name = _method_name,
method = method,
signature = ['mode'] + cls._get_method_signature,
doc = cls.get_degrees.__doc__,
)
@classmethod
def _generate_count_methods(cls):
for _get in cls._count_methods:
_get_method = getattr(cls, 'get_%s' % _get)
method_name = 'count_%s' % _get
signature = cls._update_get_method_signature(method_name)
cls._add_method(
method_name = method_name,
method = cls._count(_get_method),
signature = cls._get_method_signature,
doc = _get_method.__doc__,
)
@classmethod
def _update_get_method_signature(cls, method_name):
signature = cls._get_method_signature
if 'interaction' in method_name:
signature = signature + [
('entity_type', None),
('source_entity_type', None),
('target_entity_type', None),
]
return signature
@classmethod
def _generate_by_methods(cls):
for _get, _by in itertools.product(
cls._get_methods,
cls._by_methods,
):
_get_method = getattr(cls, 'get_%s' % _get)
method_name = '%s_by_%s' % (_get, _by)
method = getattr(cls, '_by_%s' % _by)(_get_method)
cls._add_method(
method_name = method_name,
method = method,
signature = cls._get_method_signature,
doc = _get_method.__doc__,
)
@classmethod
def _add_method(cls, method_name, method, signature = None, doc = None):
common.add_method(
cls,
method_name,
method,
signature = signature,
doc = doc,
)
[docs]
def generate_df_records(
self,
by_source: bool = False,
with_references: bool = False,
):
"""
Yields interaction records. It is a generator because one edge can
be represented by one or more records depending on the signs and
directions and other parameters
Args
by_source:
Yield separate records by resources. This way the node pairs
will be redundant and you need to group later if you want
unique interacting pairs. By default is ``False`` because for
most applications unique interactions are preferred.
If ``False`` the *refrences* field will still be present
but with ``None`` values.
with_references:
Include the literature references. By default is ``False``
because you rarely need these and they increase the data size
significantly.
"""
def source_add_via(source, via):
return '%s%s' % (source, '_%s' % via if via else '')
def iter_sources(evs):
sources = evs.get_resource_names_via()
if by_source:
for source, via in sources:
refs = (
{
ref.pmid
for ref in
self.get_references(
resources = source,
interaction_type = interaction_type,
via = via,
)
}
if with_references else
None
)
_source = source_add_via(source, via)
yield _source, refs
else:
_sources = {
source_add_via(source, via)
for source, via in sources
}
refs = (
{
ref.pmid
for ref in
self.get_references(
resources = {s[0] for s in sources},
interaction_type = interaction_type,
)
}
if with_references else
None
)
if _sources:
yield _sources, refs
for interaction_type in self.get_interaction_types():
dmodels = (
self.get_data_models(interaction_type = interaction_type)
)
dmodels = dmodels if by_source else (dmodels,)
for data_model in dmodels:
evs_undirected = self.get_evidences(
direction = 'undirected',
interaction_type = interaction_type,
data_model = data_model,
)
for _dir in (self.a_b, self.b_a):
evs_dir = self.get_evidences(
direction = _dir,
interaction_type = interaction_type,
data_model = data_model,
)
evs_without_sign = evs_dir.__copy__()
for _effect, effect in zip(
(1, -1),
('positive', 'negative')
):
evs_sign = self.get_evidences(
direction = _dir,
effect = effect,
interaction_type = interaction_type,
data_model = data_model,
)
# to make sure we keep all references:
evs_sign += evs_sign.intersection(evs_dir)
evs_without_sign -= evs_sign
evs_undirected -= evs_sign
for sources, refs in iter_sources(evs_sign):
yield InteractionDataFrameRecord(
id_a = _dir[0].identifier,
id_b = _dir[1].identifier,
type_a = _dir[0].entity_type,
type_b = _dir[1].entity_type,
directed = True,
effect = _effect,
type = interaction_type,
dmodel = data_model,
sources = sources,
references = refs,
)
if evs_without_sign:
evs_undirected -= evs_without_sign
for sources, refs in iter_sources(evs_without_sign):
yield InteractionDataFrameRecord(
id_a = _dir[0].identifier,
id_b = _dir[1].identifier,
type_a = _dir[0].entity_type,
type_b = _dir[1].entity_type,
directed = True,
effect = 0,
type = interaction_type,
dmodel = data_model,
sources = sources,
references = refs,
)
if evs_undirected:
for sources, refs in iter_sources(evs_undirected):
yield InteractionDataFrameRecord(
id_a = self.a.identifier,
id_b = self.b.identifier,
type_a = self.a.entity_type,
type_b = self.b.entity_type,
directed = False,
effect = 0,
type = interaction_type,
dmodel = data_model,
sources = sources,
references = refs,
)
[docs]
def asdict(self, direction: tuple) -> dict:
"""
Dictionary representation of the evidences.
"""
direction = self.a_b if self.a_b == direction else self.b_a
return {
'id_a': str(direction[0].identifier),
'id_b': str(direction[1].identifier),
'positive': self.positive[direction].asdict(),
'negative': self.negative[direction].asdict(),
'directed': self.unknown_effect[direction].asdict(),
'undirected': self.direction['undirected'].asdict(),
}
[docs]
def serialize_attrs(self, direction: tuple | str | None = None) -> str:
"""
Serialize the resource specific attributes into a JSON string.
"""
evs = self.evidences if not direction else self.direction[direction]
return evs.serialize_attrs()
[docs]
def serialize_evidences(self, direction: tuple) -> str:
"""
Serialize the evidences into a JSON string.
"""
return self._serialize(self.asdict(direction = direction))
def _get_attr(self, resource, key, direction):
if resource in self.direction[direction]:
return self.direction[direction][resource][key]
[docs]
def get_attr(
self,
resource: str,
key: str,
direction: tuple | str | None = None,
):
"""
Extracts the values of one specific attribute.
Args
resource:
Name of the resource.
key:
Name of the attribute.
direction:
Direction(s) to consider, either a tuple of entities or
entity names, or the string `undirected`.
Returns
Depends on the arguments. The value of the attribute if direction
is defined. Otherwise a dict with the value of the attribute for
each direction. The value of the attribute is `None` if the
resource or the attribute does not belong to this interaction.
"""
if direction:
return self._get_attr(resource, key, direction)
else:
return dict(
(
d,
self._get_attr(resource, key, d)
)
for d in self.direction.keys()
)
[docs]
def dorothea_levels(self, direction: str | tuple | None = None):
"""
Retrieves the DoRothEA confidence levels.
Args
direction:
Direction(s) to consider, either a tuple of entities or
entity names, or the string `undirected`.
Returns
List of unique single letter strings representing the five
confidence levels (A-E).
"""
directions = (direction,) if direction else (self.a_b, self.b_a)
return sorted(
{
level
for direction in directions
for level in (
self._get_attr('DoRothEA', 'level', direction) or
()
)
}
)
[docs]
def dorothea_level(
self,
direction: str | tuple,
) -> Literal['A', 'B', 'C', 'D', 'E']:
"""
DoRothEA confidence level for one direction as a single letter.
Some interactions might have multiple levels due to the ambiguous
nature of translating gene symbols to UniProt IDs. Here we take the
highest level and drop the rest. For interactions without DoRothEA
levels None is returned.
"""
levels = self.dorothea_levels(direction = direction)
return common.first(levels)
Interaction._generate_entity_methods()
Interaction._generate_get_methods()
Interaction._generate_degree_methods()
Interaction._generate_count_methods()
Interaction._generate_by_methods()