270 lines
10 KiB
Python
270 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
*****************************
|
||
Time-respecting VF2 Algorithm
|
||
*****************************
|
||
|
||
An extension of the VF2 algorithm for time-respecting graph ismorphism
|
||
testing in temporal graphs.
|
||
|
||
A temporal graph is one in which edges contain a datetime attribute,
|
||
denoting when interaction occurred between the incident nodes. A
|
||
time-respecting subgraph of a temporal graph is a subgraph such that
|
||
all interactions incident to a node occurred within a time threshold,
|
||
delta, of each other. A directed time-respecting subgraph has the
|
||
added constraint that incoming interactions to a node must precede
|
||
outgoing interactions from the same node - this enforces a sense of
|
||
directed flow.
|
||
|
||
Introduction
|
||
------------
|
||
|
||
The TimeRespectingGraphMatcher and TimeRespectingDiGraphMatcher
|
||
extend the GraphMatcher and DiGraphMatcher classes, respectively,
|
||
to include temporal constraints on matches. This is achieved through
|
||
a semantic check, via the semantic_feasibility() function.
|
||
|
||
As well as including G1 (the graph in which to seek embeddings) and
|
||
G2 (the subgraph structure of interest), the name of the temporal
|
||
attribute on the edges and the time threshold, delta, must be supplied
|
||
as arguments to the matching constructors.
|
||
|
||
A delta of zero is the strictest temporal constraint on the match -
|
||
only embeddings in which all interactions occur at the same time will
|
||
be returned. A delta of one day will allow embeddings in which
|
||
adjacent interactions occur up to a day apart.
|
||
|
||
Examples
|
||
--------
|
||
|
||
Examples will be provided when the datetime type has been incorporated.
|
||
|
||
|
||
Temporal Subgraph Isomorphism
|
||
-----------------------------
|
||
|
||
A brief discussion of the somewhat diverse current literature will be
|
||
included here.
|
||
|
||
References
|
||
----------
|
||
|
||
[1] Redmond, U. and Cunningham, P. Temporal subgraph isomorphism. In:
|
||
The 2013 IEEE/ACM International Conference on Advances in Social
|
||
Networks Analysis and Mining (ASONAM). Niagara Falls, Canada; 2013:
|
||
pages 1451 - 1452. [65]
|
||
|
||
For a discussion of the literature on temporal networks:
|
||
|
||
[3] P. Holme and J. Saramaki. Temporal networks. Physics Reports,
|
||
519(3):97–125, 2012.
|
||
|
||
Notes
|
||
-----
|
||
|
||
Handles directed and undirected graphs and graphs with parallel edges.
|
||
|
||
"""
|
||
|
||
import networkx as nx
|
||
from datetime import datetime, timedelta
|
||
from .isomorphvf2 import GraphMatcher, DiGraphMatcher
|
||
|
||
__all__ = ['TimeRespectingGraphMatcher',
|
||
'TimeRespectingDiGraphMatcher']
|
||
|
||
|
||
class TimeRespectingGraphMatcher(GraphMatcher):
|
||
|
||
def __init__(self, G1, G2, temporal_attribute_name, delta):
|
||
"""Initialize TimeRespectingGraphMatcher.
|
||
|
||
G1 and G2 should be nx.Graph or nx.MultiGraph instances.
|
||
|
||
Examples
|
||
--------
|
||
To create a TimeRespectingGraphMatcher which checks for
|
||
syntactic and semantic feasibility:
|
||
|
||
>>> from networkx.algorithms import isomorphism
|
||
>>> G1 = nx.Graph(nx.path_graph(4, create_using=nx.Graph()))
|
||
|
||
>>> G2 = nx.Graph(nx.path_graph(4, create_using=nx.Graph()))
|
||
|
||
>>> GM = isomorphism.TimeRespectingGraphMatcher(G1, G2, 'date', timedelta(days=1))
|
||
"""
|
||
self.temporal_attribute_name = temporal_attribute_name
|
||
self.delta = delta
|
||
super(TimeRespectingGraphMatcher, self).__init__(G1, G2)
|
||
|
||
def one_hop(self, Gx, Gx_node, neighbors):
|
||
"""
|
||
Edges one hop out from a node in the mapping should be
|
||
time-respecting with respect to each other.
|
||
"""
|
||
dates = []
|
||
for n in neighbors:
|
||
if type(Gx) == type(nx.Graph()): # Graph G[u][v] returns the data dictionary.
|
||
dates.append(Gx[Gx_node][n][self.temporal_attribute_name])
|
||
else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary.
|
||
for edge in Gx[Gx_node][n].values(): # Iterates all edges between node pair.
|
||
dates.append(edge[self.temporal_attribute_name])
|
||
if any(x is None for x in dates):
|
||
raise ValueError('Datetime not supplied for at least one edge.')
|
||
return not dates or max(dates) - min(dates) <= self.delta
|
||
|
||
def two_hop(self, Gx, core_x, Gx_node, neighbors):
|
||
"""
|
||
Paths of length 2 from Gx_node should be time-respecting.
|
||
"""
|
||
return all(self.one_hop(Gx, v, [n for n in Gx[v] if n in core_x] + [Gx_node]) for v in neighbors)
|
||
|
||
def semantic_feasibility(self, G1_node, G2_node):
|
||
"""Returns True if adding (G1_node, G2_node) is semantically
|
||
feasible.
|
||
|
||
Any subclass which redefines semantic_feasibility() must
|
||
maintain the self.tests if needed, to keep the match() method
|
||
functional. Implementations should consider multigraphs.
|
||
"""
|
||
neighbors = [n for n in self.G1[G1_node] if n in self.core_1]
|
||
if not self.one_hop(self.G1, G1_node, neighbors): # Fail fast on first node.
|
||
return False
|
||
if not self.two_hop(self.G1, self.core_1, G1_node, neighbors):
|
||
return False
|
||
# Otherwise, this node is semantically feasible!
|
||
return True
|
||
|
||
|
||
class TimeRespectingDiGraphMatcher(DiGraphMatcher):
|
||
|
||
def __init__(self, G1, G2, temporal_attribute_name, delta):
|
||
"""Initialize TimeRespectingDiGraphMatcher.
|
||
|
||
G1 and G2 should be nx.DiGraph or nx.MultiDiGraph instances.
|
||
|
||
Examples
|
||
--------
|
||
To create a TimeRespectingDiGraphMatcher which checks for
|
||
syntactic and semantic feasibility:
|
||
|
||
>>> from networkx.algorithms import isomorphism
|
||
>>> G1 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
|
||
|
||
>>> G2 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
|
||
|
||
>>> GM = isomorphism.TimeRespectingDiGraphMatcher(G1, G2, 'date', timedelta(days=1))
|
||
"""
|
||
self.temporal_attribute_name = temporal_attribute_name
|
||
self.delta = delta
|
||
super(TimeRespectingDiGraphMatcher, self).__init__(G1, G2)
|
||
|
||
def get_pred_dates(self, Gx, Gx_node, core_x, pred):
|
||
"""
|
||
Get the dates of edges from predecessors.
|
||
"""
|
||
pred_dates = []
|
||
if type(Gx) == type(nx.DiGraph()): # Graph G[u][v] returns the data dictionary.
|
||
for n in pred:
|
||
pred_dates.append(Gx[n][Gx_node][self.temporal_attribute_name])
|
||
else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary.
|
||
for n in pred:
|
||
for edge in Gx[n][Gx_node].values(): # Iterates all edge data between node pair.
|
||
pred_dates.append(edge[self.temporal_attribute_name])
|
||
return pred_dates
|
||
|
||
def get_succ_dates(self, Gx, Gx_node, core_x, succ):
|
||
"""
|
||
Get the dates of edges to successors.
|
||
"""
|
||
succ_dates = []
|
||
if type(Gx) == type(nx.DiGraph()): # Graph G[u][v] returns the data dictionary.
|
||
for n in succ:
|
||
succ_dates.append(Gx[Gx_node][n][self.temporal_attribute_name])
|
||
else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary.
|
||
for n in succ:
|
||
for edge in Gx[Gx_node][n].values(): # Iterates all edge data between node pair.
|
||
succ_dates.append(edge[self.temporal_attribute_name])
|
||
return succ_dates
|
||
|
||
def one_hop(self, Gx, Gx_node, core_x, pred, succ):
|
||
"""
|
||
The ego node.
|
||
"""
|
||
pred_dates = self.get_pred_dates(Gx, Gx_node, core_x, pred)
|
||
succ_dates = self.get_succ_dates(Gx, Gx_node, core_x, succ)
|
||
return self.test_one(pred_dates, succ_dates) and self.test_two(pred_dates, succ_dates)
|
||
|
||
def two_hop_pred(self, Gx, Gx_node, core_x, pred):
|
||
"""
|
||
The predeccessors of the ego node.
|
||
"""
|
||
return all(self.one_hop(Gx, p, core_x, self.preds(Gx, core_x, p), self.succs(Gx, core_x, p, Gx_node)) for p in pred)
|
||
|
||
def two_hop_succ(self, Gx, Gx_node, core_x, succ):
|
||
"""
|
||
The successors of the ego node.
|
||
"""
|
||
return all(self.one_hop(Gx, s, core_x, self.preds(Gx, core_x, s, Gx_node), self.succs(Gx, core_x, s)) for s in succ)
|
||
|
||
def preds(self, Gx, core_x, v, Gx_node=None):
|
||
pred = [n for n in Gx.predecessors(v) if n in core_x]
|
||
if Gx_node:
|
||
pred.append(Gx_node)
|
||
return pred
|
||
|
||
def succs(self, Gx, core_x, v, Gx_node=None):
|
||
succ = [n for n in Gx.successors(v) if n in core_x]
|
||
if Gx_node:
|
||
succ.append(Gx_node)
|
||
return succ
|
||
|
||
def test_one(self, pred_dates, succ_dates):
|
||
"""
|
||
Edges one hop out from Gx_node in the mapping should be
|
||
time-respecting with respect to each other, regardless of
|
||
direction.
|
||
"""
|
||
time_respecting = True
|
||
dates = pred_dates + succ_dates
|
||
|
||
if any(x is None for x in dates):
|
||
raise ValueError('Date or datetime not supplied for at least one edge.')
|
||
|
||
dates.sort() # Small to large.
|
||
if 0 < len(dates) and not (dates[-1] - dates[0] <= self.delta):
|
||
time_respecting = False
|
||
return time_respecting
|
||
|
||
def test_two(self, pred_dates, succ_dates):
|
||
"""
|
||
Edges from a dual Gx_node in the mapping should be ordered in
|
||
a time-respecting manner.
|
||
"""
|
||
time_respecting = True
|
||
pred_dates.sort()
|
||
succ_dates.sort()
|
||
# First out before last in; negative of the necessary condition for time-respect.
|
||
if 0 < len(succ_dates) and 0 < len(pred_dates) and succ_dates[0] < pred_dates[-1]:
|
||
time_respecting = False
|
||
return time_respecting
|
||
|
||
def semantic_feasibility(self, G1_node, G2_node):
|
||
"""Returns True if adding (G1_node, G2_node) is semantically
|
||
feasible.
|
||
|
||
Any subclass which redefines semantic_feasibility() must
|
||
maintain the self.tests if needed, to keep the match() method
|
||
functional. Implementations should consider multigraphs.
|
||
"""
|
||
pred, succ = [n for n in self.G1.predecessors(G1_node) if n in self.core_1], [
|
||
n for n in self.G1.successors(G1_node) if n in self.core_1]
|
||
if not self.one_hop(self.G1, G1_node, self.core_1, pred, succ): # Fail fast on first node.
|
||
return False
|
||
if not self.two_hop_pred(self.G1, G1_node, self.core_1, pred):
|
||
return False
|
||
if not self.two_hop_succ(self.G1, G1_node, self.core_1, succ):
|
||
return False
|
||
# Otherwise, this node is semantically feasible!
|
||
return True
|