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
|