Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/networkx/algorithms/isomorphism/temporalisomorphvf2.py: 22%

93 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-20 07:00 +0000

1""" 

2***************************** 

3Time-respecting VF2 Algorithm 

4***************************** 

5 

6An extension of the VF2 algorithm for time-respecting graph isomorphism 

7testing in temporal graphs. 

8 

9A temporal graph is one in which edges contain a datetime attribute, 

10denoting when interaction occurred between the incident nodes. A 

11time-respecting subgraph of a temporal graph is a subgraph such that 

12all interactions incident to a node occurred within a time threshold, 

13delta, of each other. A directed time-respecting subgraph has the 

14added constraint that incoming interactions to a node must precede 

15outgoing interactions from the same node - this enforces a sense of 

16directed flow. 

17 

18Introduction 

19------------ 

20 

21The TimeRespectingGraphMatcher and TimeRespectingDiGraphMatcher 

22extend the GraphMatcher and DiGraphMatcher classes, respectively, 

23to include temporal constraints on matches. This is achieved through 

24a semantic check, via the semantic_feasibility() function. 

25 

26As well as including G1 (the graph in which to seek embeddings) and 

27G2 (the subgraph structure of interest), the name of the temporal 

28attribute on the edges and the time threshold, delta, must be supplied 

29as arguments to the matching constructors. 

30 

31A delta of zero is the strictest temporal constraint on the match - 

32only embeddings in which all interactions occur at the same time will 

33be returned. A delta of one day will allow embeddings in which 

34adjacent interactions occur up to a day apart. 

35 

36Examples 

37-------- 

38 

39Examples will be provided when the datetime type has been incorporated. 

40 

41 

42Temporal Subgraph Isomorphism 

43----------------------------- 

44 

45A brief discussion of the somewhat diverse current literature will be 

46included here. 

47 

48References 

49---------- 

50 

51[1] Redmond, U. and Cunningham, P. Temporal subgraph isomorphism. In: 

52The 2013 IEEE/ACM International Conference on Advances in Social 

53Networks Analysis and Mining (ASONAM). Niagara Falls, Canada; 2013: 

54pages 1451 - 1452. [65] 

55 

56For a discussion of the literature on temporal networks: 

57 

58[3] P. Holme and J. Saramaki. Temporal networks. Physics Reports, 

59519(3):97–125, 2012. 

60 

61Notes 

62----- 

63 

64Handles directed and undirected graphs and graphs with parallel edges. 

65 

66""" 

67 

68import networkx as nx 

69 

70from .isomorphvf2 import DiGraphMatcher, GraphMatcher 

71 

72__all__ = ["TimeRespectingGraphMatcher", "TimeRespectingDiGraphMatcher"] 

73 

74 

75class TimeRespectingGraphMatcher(GraphMatcher): 

76 def __init__(self, G1, G2, temporal_attribute_name, delta): 

77 """Initialize TimeRespectingGraphMatcher. 

78 

79 G1 and G2 should be nx.Graph or nx.MultiGraph instances. 

80 

81 Examples 

82 -------- 

83 To create a TimeRespectingGraphMatcher which checks for 

84 syntactic and semantic feasibility: 

85 

86 >>> from networkx.algorithms import isomorphism 

87 >>> from datetime import timedelta 

88 >>> G1 = nx.Graph(nx.path_graph(4, create_using=nx.Graph())) 

89 

90 >>> G2 = nx.Graph(nx.path_graph(4, create_using=nx.Graph())) 

91 

92 >>> GM = isomorphism.TimeRespectingGraphMatcher( 

93 ... G1, G2, "date", timedelta(days=1) 

94 ... ) 

95 """ 

96 self.temporal_attribute_name = temporal_attribute_name 

97 self.delta = delta 

98 super().__init__(G1, G2) 

99 

100 def one_hop(self, Gx, Gx_node, neighbors): 

101 """ 

102 Edges one hop out from a node in the mapping should be 

103 time-respecting with respect to each other. 

104 """ 

105 dates = [] 

106 for n in neighbors: 

107 if isinstance(Gx, nx.Graph): # Graph G[u][v] returns the data dictionary. 

108 dates.append(Gx[Gx_node][n][self.temporal_attribute_name]) 

109 else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary. 

110 for edge in Gx[Gx_node][ 

111 n 

112 ].values(): # Iterates all edges between node pair. 

113 dates.append(edge[self.temporal_attribute_name]) 

114 if any(x is None for x in dates): 

115 raise ValueError("Datetime not supplied for at least one edge.") 

116 return not dates or max(dates) - min(dates) <= self.delta 

117 

118 def two_hop(self, Gx, core_x, Gx_node, neighbors): 

119 """ 

120 Paths of length 2 from Gx_node should be time-respecting. 

121 """ 

122 return all( 

123 self.one_hop(Gx, v, [n for n in Gx[v] if n in core_x] + [Gx_node]) 

124 for v in neighbors 

125 ) 

126 

127 def semantic_feasibility(self, G1_node, G2_node): 

128 """Returns True if adding (G1_node, G2_node) is semantically 

129 feasible. 

130 

131 Any subclass which redefines semantic_feasibility() must 

132 maintain the self.tests if needed, to keep the match() method 

133 functional. Implementations should consider multigraphs. 

134 """ 

135 neighbors = [n for n in self.G1[G1_node] if n in self.core_1] 

136 if not self.one_hop(self.G1, G1_node, neighbors): # Fail fast on first node. 

137 return False 

138 if not self.two_hop(self.G1, self.core_1, G1_node, neighbors): 

139 return False 

140 # Otherwise, this node is semantically feasible! 

141 return True 

142 

143 

144class TimeRespectingDiGraphMatcher(DiGraphMatcher): 

145 def __init__(self, G1, G2, temporal_attribute_name, delta): 

146 """Initialize TimeRespectingDiGraphMatcher. 

147 

148 G1 and G2 should be nx.DiGraph or nx.MultiDiGraph instances. 

149 

150 Examples 

151 -------- 

152 To create a TimeRespectingDiGraphMatcher which checks for 

153 syntactic and semantic feasibility: 

154 

155 >>> from networkx.algorithms import isomorphism 

156 >>> from datetime import timedelta 

157 >>> G1 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph())) 

158 

159 >>> G2 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph())) 

160 

161 >>> GM = isomorphism.TimeRespectingDiGraphMatcher( 

162 ... G1, G2, "date", timedelta(days=1) 

163 ... ) 

164 """ 

165 self.temporal_attribute_name = temporal_attribute_name 

166 self.delta = delta 

167 super().__init__(G1, G2) 

168 

169 def get_pred_dates(self, Gx, Gx_node, core_x, pred): 

170 """ 

171 Get the dates of edges from predecessors. 

172 """ 

173 pred_dates = [] 

174 if isinstance(Gx, nx.DiGraph): # Graph G[u][v] returns the data dictionary. 

175 for n in pred: 

176 pred_dates.append(Gx[n][Gx_node][self.temporal_attribute_name]) 

177 else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary. 

178 for n in pred: 

179 for edge in Gx[n][ 

180 Gx_node 

181 ].values(): # Iterates all edge data between node pair. 

182 pred_dates.append(edge[self.temporal_attribute_name]) 

183 return pred_dates 

184 

185 def get_succ_dates(self, Gx, Gx_node, core_x, succ): 

186 """ 

187 Get the dates of edges to successors. 

188 """ 

189 succ_dates = [] 

190 if isinstance(Gx, nx.DiGraph): # Graph G[u][v] returns the data dictionary. 

191 for n in succ: 

192 succ_dates.append(Gx[Gx_node][n][self.temporal_attribute_name]) 

193 else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary. 

194 for n in succ: 

195 for edge in Gx[Gx_node][ 

196 n 

197 ].values(): # Iterates all edge data between node pair. 

198 succ_dates.append(edge[self.temporal_attribute_name]) 

199 return succ_dates 

200 

201 def one_hop(self, Gx, Gx_node, core_x, pred, succ): 

202 """ 

203 The ego node. 

204 """ 

205 pred_dates = self.get_pred_dates(Gx, Gx_node, core_x, pred) 

206 succ_dates = self.get_succ_dates(Gx, Gx_node, core_x, succ) 

207 return self.test_one(pred_dates, succ_dates) and self.test_two( 

208 pred_dates, succ_dates 

209 ) 

210 

211 def two_hop_pred(self, Gx, Gx_node, core_x, pred): 

212 """ 

213 The predecessors of the ego node. 

214 """ 

215 return all( 

216 self.one_hop( 

217 Gx, 

218 p, 

219 core_x, 

220 self.preds(Gx, core_x, p), 

221 self.succs(Gx, core_x, p, Gx_node), 

222 ) 

223 for p in pred 

224 ) 

225 

226 def two_hop_succ(self, Gx, Gx_node, core_x, succ): 

227 """ 

228 The successors of the ego node. 

229 """ 

230 return all( 

231 self.one_hop( 

232 Gx, 

233 s, 

234 core_x, 

235 self.preds(Gx, core_x, s, Gx_node), 

236 self.succs(Gx, core_x, s), 

237 ) 

238 for s in succ 

239 ) 

240 

241 def preds(self, Gx, core_x, v, Gx_node=None): 

242 pred = [n for n in Gx.predecessors(v) if n in core_x] 

243 if Gx_node: 

244 pred.append(Gx_node) 

245 return pred 

246 

247 def succs(self, Gx, core_x, v, Gx_node=None): 

248 succ = [n for n in Gx.successors(v) if n in core_x] 

249 if Gx_node: 

250 succ.append(Gx_node) 

251 return succ 

252 

253 def test_one(self, pred_dates, succ_dates): 

254 """ 

255 Edges one hop out from Gx_node in the mapping should be 

256 time-respecting with respect to each other, regardless of 

257 direction. 

258 """ 

259 time_respecting = True 

260 dates = pred_dates + succ_dates 

261 

262 if any(x is None for x in dates): 

263 raise ValueError("Date or datetime not supplied for at least one edge.") 

264 

265 dates.sort() # Small to large. 

266 if 0 < len(dates) and not (dates[-1] - dates[0] <= self.delta): 

267 time_respecting = False 

268 return time_respecting 

269 

270 def test_two(self, pred_dates, succ_dates): 

271 """ 

272 Edges from a dual Gx_node in the mapping should be ordered in 

273 a time-respecting manner. 

274 """ 

275 time_respecting = True 

276 pred_dates.sort() 

277 succ_dates.sort() 

278 # First out before last in; negative of the necessary condition for time-respect. 

279 if ( 

280 0 < len(succ_dates) 

281 and 0 < len(pred_dates) 

282 and succ_dates[0] < pred_dates[-1] 

283 ): 

284 time_respecting = False 

285 return time_respecting 

286 

287 def semantic_feasibility(self, G1_node, G2_node): 

288 """Returns True if adding (G1_node, G2_node) is semantically 

289 feasible. 

290 

291 Any subclass which redefines semantic_feasibility() must 

292 maintain the self.tests if needed, to keep the match() method 

293 functional. Implementations should consider multigraphs. 

294 """ 

295 pred, succ = ( 

296 [n for n in self.G1.predecessors(G1_node) if n in self.core_1], 

297 [n for n in self.G1.successors(G1_node) if n in self.core_1], 

298 ) 

299 if not self.one_hop( 

300 self.G1, G1_node, self.core_1, pred, succ 

301 ): # Fail fast on first node. 

302 return False 

303 if not self.two_hop_pred(self.G1, G1_node, self.core_1, pred): 

304 return False 

305 if not self.two_hop_succ(self.G1, G1_node, self.core_1, succ): 

306 return False 

307 # Otherwise, this node is semantically feasible! 

308 return True