isomorphvf2.py 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. """
  2. *************
  3. VF2 Algorithm
  4. *************
  5. An implementation of VF2 algorithm for graph isomorphism testing.
  6. The simplest interface to use this module is to call networkx.is_isomorphic().
  7. Introduction
  8. ------------
  9. The GraphMatcher and DiGraphMatcher are responsible for matching
  10. graphs or directed graphs in a predetermined manner. This
  11. usually means a check for an isomorphism, though other checks
  12. are also possible. For example, a subgraph of one graph
  13. can be checked for isomorphism to a second graph.
  14. Matching is done via syntactic feasibility. It is also possible
  15. to check for semantic feasibility. Feasibility, then, is defined
  16. as the logical AND of the two functions.
  17. To include a semantic check, the (Di)GraphMatcher class should be
  18. subclassed, and the semantic_feasibility() function should be
  19. redefined. By default, the semantic feasibility function always
  20. returns True. The effect of this is that semantics are not
  21. considered in the matching of G1 and G2.
  22. Examples
  23. --------
  24. Suppose G1 and G2 are isomorphic graphs. Verification is as follows:
  25. >>> from networkx.algorithms import isomorphism
  26. >>> G1 = nx.path_graph(4)
  27. >>> G2 = nx.path_graph(4)
  28. >>> GM = isomorphism.GraphMatcher(G1, G2)
  29. >>> GM.is_isomorphic()
  30. True
  31. GM.mapping stores the isomorphism mapping from G1 to G2.
  32. >>> GM.mapping
  33. {0: 0, 1: 1, 2: 2, 3: 3}
  34. Suppose G1 and G2 are isomorphic directed graphs.
  35. Verification is as follows:
  36. >>> G1 = nx.path_graph(4, create_using=nx.DiGraph())
  37. >>> G2 = nx.path_graph(4, create_using=nx.DiGraph())
  38. >>> DiGM = isomorphism.DiGraphMatcher(G1, G2)
  39. >>> DiGM.is_isomorphic()
  40. True
  41. DiGM.mapping stores the isomorphism mapping from G1 to G2.
  42. >>> DiGM.mapping
  43. {0: 0, 1: 1, 2: 2, 3: 3}
  44. Subgraph Isomorphism
  45. --------------------
  46. Graph theory literature can be ambiguous about the meaning of the
  47. above statement, and we seek to clarify it now.
  48. In the VF2 literature, a mapping M is said to be a graph-subgraph
  49. isomorphism iff M is an isomorphism between G2 and a subgraph of G1.
  50. Thus, to say that G1 and G2 are graph-subgraph isomorphic is to say
  51. that a subgraph of G1 is isomorphic to G2.
  52. Other literature uses the phrase 'subgraph isomorphic' as in 'G1 does
  53. not have a subgraph isomorphic to G2'. Another use is as an in adverb
  54. for isomorphic. Thus, to say that G1 and G2 are subgraph isomorphic
  55. is to say that a subgraph of G1 is isomorphic to G2.
  56. Finally, the term 'subgraph' can have multiple meanings. In this
  57. context, 'subgraph' always means a 'node-induced subgraph'. Edge-induced
  58. subgraph isomorphisms are not directly supported, but one should be
  59. able to perform the check by making use of nx.line_graph(). For
  60. subgraphs which are not induced, the term 'monomorphism' is preferred
  61. over 'isomorphism'.
  62. Let G=(N,E) be a graph with a set of nodes N and set of edges E.
  63. If G'=(N',E') is a subgraph, then:
  64. N' is a subset of N
  65. E' is a subset of E
  66. If G'=(N',E') is a node-induced subgraph, then:
  67. N' is a subset of N
  68. E' is the subset of edges in E relating nodes in N'
  69. If G'=(N',E') is an edge-induced subgraph, then:
  70. N' is the subset of nodes in N related by edges in E'
  71. E' is a subset of E
  72. If G'=(N',E') is a monomorphism, then:
  73. N' is a subset of N
  74. E' is a subset of the set of edges in E relating nodes in N'
  75. Note that if G' is a node-induced subgraph of G, then it is always a
  76. subgraph monomorphism of G, but the opposite is not always true, as a
  77. monomorphism can have fewer edges.
  78. References
  79. ----------
  80. [1] Luigi P. Cordella, Pasquale Foggia, Carlo Sansone, Mario Vento,
  81. "A (Sub)Graph Isomorphism Algorithm for Matching Large Graphs",
  82. IEEE Transactions on Pattern Analysis and Machine Intelligence,
  83. vol. 26, no. 10, pp. 1367-1372, Oct., 2004.
  84. http://ieeexplore.ieee.org/iel5/34/29305/01323804.pdf
  85. [2] L. P. Cordella, P. Foggia, C. Sansone, M. Vento, "An Improved
  86. Algorithm for Matching Large Graphs", 3rd IAPR-TC15 Workshop
  87. on Graph-based Representations in Pattern Recognition, Cuen,
  88. pp. 149-159, 2001.
  89. https://www.researchgate.net/publication/200034365_An_Improved_Algorithm_for_Matching_Large_Graphs
  90. See Also
  91. --------
  92. syntactic_feasibility(), semantic_feasibility()
  93. Notes
  94. -----
  95. The implementation handles both directed and undirected graphs as well
  96. as multigraphs.
  97. In general, the subgraph isomorphism problem is NP-complete whereas the
  98. graph isomorphism problem is most likely not NP-complete (although no
  99. polynomial-time algorithm is known to exist).
  100. """
  101. # This work was originally coded by Christopher Ellison
  102. # as part of the Computational Mechanics Python (CMPy) project.
  103. # James P. Crutchfield, principal investigator.
  104. # Complexity Sciences Center and Physics Department, UC Davis.
  105. import sys
  106. __all__ = ["GraphMatcher", "DiGraphMatcher"]
  107. class GraphMatcher:
  108. """Implementation of VF2 algorithm for matching undirected graphs.
  109. Suitable for Graph and MultiGraph instances.
  110. """
  111. def __init__(self, G1, G2):
  112. """Initialize GraphMatcher.
  113. Parameters
  114. ----------
  115. G1,G2: NetworkX Graph or MultiGraph instances.
  116. The two graphs to check for isomorphism or monomorphism.
  117. Examples
  118. --------
  119. To create a GraphMatcher which checks for syntactic feasibility:
  120. >>> from networkx.algorithms import isomorphism
  121. >>> G1 = nx.path_graph(4)
  122. >>> G2 = nx.path_graph(4)
  123. >>> GM = isomorphism.GraphMatcher(G1, G2)
  124. """
  125. self.G1 = G1
  126. self.G2 = G2
  127. self.G1_nodes = set(G1.nodes())
  128. self.G2_nodes = set(G2.nodes())
  129. self.G2_node_order = {n: i for i, n in enumerate(G2)}
  130. # Set recursion limit.
  131. self.old_recursion_limit = sys.getrecursionlimit()
  132. expected_max_recursion_level = len(self.G2)
  133. if self.old_recursion_limit < 1.5 * expected_max_recursion_level:
  134. # Give some breathing room.
  135. sys.setrecursionlimit(int(1.5 * expected_max_recursion_level))
  136. # Declare that we will be searching for a graph-graph isomorphism.
  137. self.test = "graph"
  138. # Initialize state
  139. self.initialize()
  140. def reset_recursion_limit(self):
  141. """Restores the recursion limit."""
  142. # TODO:
  143. # Currently, we use recursion and set the recursion level higher.
  144. # It would be nice to restore the level, but because the
  145. # (Di)GraphMatcher classes make use of cyclic references, garbage
  146. # collection will never happen when we define __del__() to
  147. # restore the recursion level. The result is a memory leak.
  148. # So for now, we do not automatically restore the recursion level,
  149. # and instead provide a method to do this manually. Eventually,
  150. # we should turn this into a non-recursive implementation.
  151. sys.setrecursionlimit(self.old_recursion_limit)
  152. def candidate_pairs_iter(self):
  153. """Iterator over candidate pairs of nodes in G1 and G2."""
  154. # All computations are done using the current state!
  155. G1_nodes = self.G1_nodes
  156. G2_nodes = self.G2_nodes
  157. min_key = self.G2_node_order.__getitem__
  158. # First we compute the inout-terminal sets.
  159. T1_inout = [node for node in self.inout_1 if node not in self.core_1]
  160. T2_inout = [node for node in self.inout_2 if node not in self.core_2]
  161. # If T1_inout and T2_inout are both nonempty.
  162. # P(s) = T1_inout x {min T2_inout}
  163. if T1_inout and T2_inout:
  164. node_2 = min(T2_inout, key=min_key)
  165. for node_1 in T1_inout:
  166. yield node_1, node_2
  167. else:
  168. # If T1_inout and T2_inout were both empty....
  169. # P(s) = (N_1 - M_1) x {min (N_2 - M_2)}
  170. # if not (T1_inout or T2_inout): # as suggested by [2], incorrect
  171. if 1: # as inferred from [1], correct
  172. # First we determine the candidate node for G2
  173. other_node = min(G2_nodes - set(self.core_2), key=min_key)
  174. for node in self.G1:
  175. if node not in self.core_1:
  176. yield node, other_node
  177. # For all other cases, we don't have any candidate pairs.
  178. def initialize(self):
  179. """Reinitializes the state of the algorithm.
  180. This method should be redefined if using something other than GMState.
  181. If only subclassing GraphMatcher, a redefinition is not necessary.
  182. """
  183. # core_1[n] contains the index of the node paired with n, which is m,
  184. # provided n is in the mapping.
  185. # core_2[m] contains the index of the node paired with m, which is n,
  186. # provided m is in the mapping.
  187. self.core_1 = {}
  188. self.core_2 = {}
  189. # See the paper for definitions of M_x and T_x^{y}
  190. # inout_1[n] is non-zero if n is in M_1 or in T_1^{inout}
  191. # inout_2[m] is non-zero if m is in M_2 or in T_2^{inout}
  192. #
  193. # The value stored is the depth of the SSR tree when the node became
  194. # part of the corresponding set.
  195. self.inout_1 = {}
  196. self.inout_2 = {}
  197. # Practically, these sets simply store the nodes in the subgraph.
  198. self.state = GMState(self)
  199. # Provide a convenient way to access the isomorphism mapping.
  200. self.mapping = self.core_1.copy()
  201. def is_isomorphic(self):
  202. """Returns True if G1 and G2 are isomorphic graphs."""
  203. # Let's do two very quick checks!
  204. # QUESTION: Should we call faster_graph_could_be_isomorphic(G1,G2)?
  205. # For now, I just copy the code.
  206. # Check global properties
  207. if self.G1.order() != self.G2.order():
  208. return False
  209. # Check local properties
  210. d1 = sorted(d for n, d in self.G1.degree())
  211. d2 = sorted(d for n, d in self.G2.degree())
  212. if d1 != d2:
  213. return False
  214. try:
  215. x = next(self.isomorphisms_iter())
  216. return True
  217. except StopIteration:
  218. return False
  219. def isomorphisms_iter(self):
  220. """Generator over isomorphisms between G1 and G2."""
  221. # Declare that we are looking for a graph-graph isomorphism.
  222. self.test = "graph"
  223. self.initialize()
  224. yield from self.match()
  225. def match(self):
  226. """Extends the isomorphism mapping.
  227. This function is called recursively to determine if a complete
  228. isomorphism can be found between G1 and G2. It cleans up the class
  229. variables after each recursive call. If an isomorphism is found,
  230. we yield the mapping.
  231. """
  232. if len(self.core_1) == len(self.G2):
  233. # Save the final mapping, otherwise garbage collection deletes it.
  234. self.mapping = self.core_1.copy()
  235. # The mapping is complete.
  236. yield self.mapping
  237. else:
  238. for G1_node, G2_node in self.candidate_pairs_iter():
  239. if self.syntactic_feasibility(G1_node, G2_node):
  240. if self.semantic_feasibility(G1_node, G2_node):
  241. # Recursive call, adding the feasible state.
  242. newstate = self.state.__class__(self, G1_node, G2_node)
  243. yield from self.match()
  244. # restore data structures
  245. newstate.restore()
  246. def semantic_feasibility(self, G1_node, G2_node):
  247. """Returns True if adding (G1_node, G2_node) is symantically feasible.
  248. The semantic feasibility function should return True if it is
  249. acceptable to add the candidate pair (G1_node, G2_node) to the current
  250. partial isomorphism mapping. The logic should focus on semantic
  251. information contained in the edge data or a formalized node class.
  252. By acceptable, we mean that the subsequent mapping can still become a
  253. complete isomorphism mapping. Thus, if adding the candidate pair
  254. definitely makes it so that the subsequent mapping cannot become a
  255. complete isomorphism mapping, then this function must return False.
  256. The default semantic feasibility function always returns True. The
  257. effect is that semantics are not considered in the matching of G1
  258. and G2.
  259. The semantic checks might differ based on the what type of test is
  260. being performed. A keyword description of the test is stored in
  261. self.test. Here is a quick description of the currently implemented
  262. tests::
  263. test='graph'
  264. Indicates that the graph matcher is looking for a graph-graph
  265. isomorphism.
  266. test='subgraph'
  267. Indicates that the graph matcher is looking for a subgraph-graph
  268. isomorphism such that a subgraph of G1 is isomorphic to G2.
  269. test='mono'
  270. Indicates that the graph matcher is looking for a subgraph-graph
  271. monomorphism such that a subgraph of G1 is monomorphic to G2.
  272. Any subclass which redefines semantic_feasibility() must maintain
  273. the above form to keep the match() method functional. Implementations
  274. should consider multigraphs.
  275. """
  276. return True
  277. def subgraph_is_isomorphic(self):
  278. """Returns True if a subgraph of G1 is isomorphic to G2."""
  279. try:
  280. x = next(self.subgraph_isomorphisms_iter())
  281. return True
  282. except StopIteration:
  283. return False
  284. def subgraph_is_monomorphic(self):
  285. """Returns True if a subgraph of G1 is monomorphic to G2."""
  286. try:
  287. x = next(self.subgraph_monomorphisms_iter())
  288. return True
  289. except StopIteration:
  290. return False
  291. # subgraph_is_isomorphic.__doc__ += "\n" + subgraph.replace('\n','\n'+indent)
  292. def subgraph_isomorphisms_iter(self):
  293. """Generator over isomorphisms between a subgraph of G1 and G2."""
  294. # Declare that we are looking for graph-subgraph isomorphism.
  295. self.test = "subgraph"
  296. self.initialize()
  297. yield from self.match()
  298. def subgraph_monomorphisms_iter(self):
  299. """Generator over monomorphisms between a subgraph of G1 and G2."""
  300. # Declare that we are looking for graph-subgraph monomorphism.
  301. self.test = "mono"
  302. self.initialize()
  303. yield from self.match()
  304. # subgraph_isomorphisms_iter.__doc__ += "\n" + subgraph.replace('\n','\n'+indent)
  305. def syntactic_feasibility(self, G1_node, G2_node):
  306. """Returns True if adding (G1_node, G2_node) is syntactically feasible.
  307. This function returns True if it is adding the candidate pair
  308. to the current partial isomorphism/monomorphism mapping is allowable.
  309. The addition is allowable if the inclusion of the candidate pair does
  310. not make it impossible for an isomorphism/monomorphism to be found.
  311. """
  312. # The VF2 algorithm was designed to work with graphs having, at most,
  313. # one edge connecting any two nodes. This is not the case when
  314. # dealing with an MultiGraphs.
  315. #
  316. # Basically, when we test the look-ahead rules R_neighbor, we will
  317. # make sure that the number of edges are checked. We also add
  318. # a R_self check to verify that the number of selfloops is acceptable.
  319. #
  320. # Users might be comparing Graph instances with MultiGraph instances.
  321. # So the generic GraphMatcher class must work with MultiGraphs.
  322. # Care must be taken since the value in the innermost dictionary is a
  323. # singlet for Graph instances. For MultiGraphs, the value in the
  324. # innermost dictionary is a list.
  325. ###
  326. # Test at each step to get a return value as soon as possible.
  327. ###
  328. # Look ahead 0
  329. # R_self
  330. # The number of selfloops for G1_node must equal the number of
  331. # self-loops for G2_node. Without this check, we would fail on
  332. # R_neighbor at the next recursion level. But it is good to prune the
  333. # search tree now.
  334. if self.test == "mono":
  335. if self.G1.number_of_edges(G1_node, G1_node) < self.G2.number_of_edges(
  336. G2_node, G2_node
  337. ):
  338. return False
  339. else:
  340. if self.G1.number_of_edges(G1_node, G1_node) != self.G2.number_of_edges(
  341. G2_node, G2_node
  342. ):
  343. return False
  344. # R_neighbor
  345. # For each neighbor n' of n in the partial mapping, the corresponding
  346. # node m' is a neighbor of m, and vice versa. Also, the number of
  347. # edges must be equal.
  348. if self.test != "mono":
  349. for neighbor in self.G1[G1_node]:
  350. if neighbor in self.core_1:
  351. if self.core_1[neighbor] not in self.G2[G2_node]:
  352. return False
  353. elif self.G1.number_of_edges(
  354. neighbor, G1_node
  355. ) != self.G2.number_of_edges(self.core_1[neighbor], G2_node):
  356. return False
  357. for neighbor in self.G2[G2_node]:
  358. if neighbor in self.core_2:
  359. if self.core_2[neighbor] not in self.G1[G1_node]:
  360. return False
  361. elif self.test == "mono":
  362. if self.G1.number_of_edges(
  363. self.core_2[neighbor], G1_node
  364. ) < self.G2.number_of_edges(neighbor, G2_node):
  365. return False
  366. else:
  367. if self.G1.number_of_edges(
  368. self.core_2[neighbor], G1_node
  369. ) != self.G2.number_of_edges(neighbor, G2_node):
  370. return False
  371. if self.test != "mono":
  372. # Look ahead 1
  373. # R_terminout
  374. # The number of neighbors of n in T_1^{inout} is equal to the
  375. # number of neighbors of m that are in T_2^{inout}, and vice versa.
  376. num1 = 0
  377. for neighbor in self.G1[G1_node]:
  378. if (neighbor in self.inout_1) and (neighbor not in self.core_1):
  379. num1 += 1
  380. num2 = 0
  381. for neighbor in self.G2[G2_node]:
  382. if (neighbor in self.inout_2) and (neighbor not in self.core_2):
  383. num2 += 1
  384. if self.test == "graph":
  385. if num1 != num2:
  386. return False
  387. else: # self.test == 'subgraph'
  388. if not (num1 >= num2):
  389. return False
  390. # Look ahead 2
  391. # R_new
  392. # The number of neighbors of n that are neither in the core_1 nor
  393. # T_1^{inout} is equal to the number of neighbors of m
  394. # that are neither in core_2 nor T_2^{inout}.
  395. num1 = 0
  396. for neighbor in self.G1[G1_node]:
  397. if neighbor not in self.inout_1:
  398. num1 += 1
  399. num2 = 0
  400. for neighbor in self.G2[G2_node]:
  401. if neighbor not in self.inout_2:
  402. num2 += 1
  403. if self.test == "graph":
  404. if num1 != num2:
  405. return False
  406. else: # self.test == 'subgraph'
  407. if not (num1 >= num2):
  408. return False
  409. # Otherwise, this node pair is syntactically feasible!
  410. return True
  411. class DiGraphMatcher(GraphMatcher):
  412. """Implementation of VF2 algorithm for matching directed graphs.
  413. Suitable for DiGraph and MultiDiGraph instances.
  414. """
  415. def __init__(self, G1, G2):
  416. """Initialize DiGraphMatcher.
  417. G1 and G2 should be nx.Graph or nx.MultiGraph instances.
  418. Examples
  419. --------
  420. To create a GraphMatcher which checks for syntactic feasibility:
  421. >>> from networkx.algorithms import isomorphism
  422. >>> G1 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
  423. >>> G2 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph()))
  424. >>> DiGM = isomorphism.DiGraphMatcher(G1, G2)
  425. """
  426. super().__init__(G1, G2)
  427. def candidate_pairs_iter(self):
  428. """Iterator over candidate pairs of nodes in G1 and G2."""
  429. # All computations are done using the current state!
  430. G1_nodes = self.G1_nodes
  431. G2_nodes = self.G2_nodes
  432. min_key = self.G2_node_order.__getitem__
  433. # First we compute the out-terminal sets.
  434. T1_out = [node for node in self.out_1 if node not in self.core_1]
  435. T2_out = [node for node in self.out_2 if node not in self.core_2]
  436. # If T1_out and T2_out are both nonempty.
  437. # P(s) = T1_out x {min T2_out}
  438. if T1_out and T2_out:
  439. node_2 = min(T2_out, key=min_key)
  440. for node_1 in T1_out:
  441. yield node_1, node_2
  442. # If T1_out and T2_out were both empty....
  443. # We compute the in-terminal sets.
  444. # elif not (T1_out or T2_out): # as suggested by [2], incorrect
  445. else: # as suggested by [1], correct
  446. T1_in = [node for node in self.in_1 if node not in self.core_1]
  447. T2_in = [node for node in self.in_2 if node not in self.core_2]
  448. # If T1_in and T2_in are both nonempty.
  449. # P(s) = T1_out x {min T2_out}
  450. if T1_in and T2_in:
  451. node_2 = min(T2_in, key=min_key)
  452. for node_1 in T1_in:
  453. yield node_1, node_2
  454. # If all terminal sets are empty...
  455. # P(s) = (N_1 - M_1) x {min (N_2 - M_2)}
  456. # elif not (T1_in or T2_in): # as suggested by [2], incorrect
  457. else: # as inferred from [1], correct
  458. node_2 = min(G2_nodes - set(self.core_2), key=min_key)
  459. for node_1 in G1_nodes:
  460. if node_1 not in self.core_1:
  461. yield node_1, node_2
  462. # For all other cases, we don't have any candidate pairs.
  463. def initialize(self):
  464. """Reinitializes the state of the algorithm.
  465. This method should be redefined if using something other than DiGMState.
  466. If only subclassing GraphMatcher, a redefinition is not necessary.
  467. """
  468. # core_1[n] contains the index of the node paired with n, which is m,
  469. # provided n is in the mapping.
  470. # core_2[m] contains the index of the node paired with m, which is n,
  471. # provided m is in the mapping.
  472. self.core_1 = {}
  473. self.core_2 = {}
  474. # See the paper for definitions of M_x and T_x^{y}
  475. # in_1[n] is non-zero if n is in M_1 or in T_1^{in}
  476. # out_1[n] is non-zero if n is in M_1 or in T_1^{out}
  477. #
  478. # in_2[m] is non-zero if m is in M_2 or in T_2^{in}
  479. # out_2[m] is non-zero if m is in M_2 or in T_2^{out}
  480. #
  481. # The value stored is the depth of the search tree when the node became
  482. # part of the corresponding set.
  483. self.in_1 = {}
  484. self.in_2 = {}
  485. self.out_1 = {}
  486. self.out_2 = {}
  487. self.state = DiGMState(self)
  488. # Provide a convenient way to access the isomorphism mapping.
  489. self.mapping = self.core_1.copy()
  490. def syntactic_feasibility(self, G1_node, G2_node):
  491. """Returns True if adding (G1_node, G2_node) is syntactically feasible.
  492. This function returns True if it is adding the candidate pair
  493. to the current partial isomorphism/monomorphism mapping is allowable.
  494. The addition is allowable if the inclusion of the candidate pair does
  495. not make it impossible for an isomorphism/monomorphism to be found.
  496. """
  497. # The VF2 algorithm was designed to work with graphs having, at most,
  498. # one edge connecting any two nodes. This is not the case when
  499. # dealing with an MultiGraphs.
  500. #
  501. # Basically, when we test the look-ahead rules R_pred and R_succ, we
  502. # will make sure that the number of edges are checked. We also add
  503. # a R_self check to verify that the number of selfloops is acceptable.
  504. # Users might be comparing DiGraph instances with MultiDiGraph
  505. # instances. So the generic DiGraphMatcher class must work with
  506. # MultiDiGraphs. Care must be taken since the value in the innermost
  507. # dictionary is a singlet for DiGraph instances. For MultiDiGraphs,
  508. # the value in the innermost dictionary is a list.
  509. ###
  510. # Test at each step to get a return value as soon as possible.
  511. ###
  512. # Look ahead 0
  513. # R_self
  514. # The number of selfloops for G1_node must equal the number of
  515. # self-loops for G2_node. Without this check, we would fail on R_pred
  516. # at the next recursion level. This should prune the tree even further.
  517. if self.test == "mono":
  518. if self.G1.number_of_edges(G1_node, G1_node) < self.G2.number_of_edges(
  519. G2_node, G2_node
  520. ):
  521. return False
  522. else:
  523. if self.G1.number_of_edges(G1_node, G1_node) != self.G2.number_of_edges(
  524. G2_node, G2_node
  525. ):
  526. return False
  527. # R_pred
  528. # For each predecessor n' of n in the partial mapping, the
  529. # corresponding node m' is a predecessor of m, and vice versa. Also,
  530. # the number of edges must be equal
  531. if self.test != "mono":
  532. for predecessor in self.G1.pred[G1_node]:
  533. if predecessor in self.core_1:
  534. if self.core_1[predecessor] not in self.G2.pred[G2_node]:
  535. return False
  536. elif self.G1.number_of_edges(
  537. predecessor, G1_node
  538. ) != self.G2.number_of_edges(self.core_1[predecessor], G2_node):
  539. return False
  540. for predecessor in self.G2.pred[G2_node]:
  541. if predecessor in self.core_2:
  542. if self.core_2[predecessor] not in self.G1.pred[G1_node]:
  543. return False
  544. elif self.test == "mono":
  545. if self.G1.number_of_edges(
  546. self.core_2[predecessor], G1_node
  547. ) < self.G2.number_of_edges(predecessor, G2_node):
  548. return False
  549. else:
  550. if self.G1.number_of_edges(
  551. self.core_2[predecessor], G1_node
  552. ) != self.G2.number_of_edges(predecessor, G2_node):
  553. return False
  554. # R_succ
  555. # For each successor n' of n in the partial mapping, the corresponding
  556. # node m' is a successor of m, and vice versa. Also, the number of
  557. # edges must be equal.
  558. if self.test != "mono":
  559. for successor in self.G1[G1_node]:
  560. if successor in self.core_1:
  561. if self.core_1[successor] not in self.G2[G2_node]:
  562. return False
  563. elif self.G1.number_of_edges(
  564. G1_node, successor
  565. ) != self.G2.number_of_edges(G2_node, self.core_1[successor]):
  566. return False
  567. for successor in self.G2[G2_node]:
  568. if successor in self.core_2:
  569. if self.core_2[successor] not in self.G1[G1_node]:
  570. return False
  571. elif self.test == "mono":
  572. if self.G1.number_of_edges(
  573. G1_node, self.core_2[successor]
  574. ) < self.G2.number_of_edges(G2_node, successor):
  575. return False
  576. else:
  577. if self.G1.number_of_edges(
  578. G1_node, self.core_2[successor]
  579. ) != self.G2.number_of_edges(G2_node, successor):
  580. return False
  581. if self.test != "mono":
  582. # Look ahead 1
  583. # R_termin
  584. # The number of predecessors of n that are in T_1^{in} is equal to the
  585. # number of predecessors of m that are in T_2^{in}.
  586. num1 = 0
  587. for predecessor in self.G1.pred[G1_node]:
  588. if (predecessor in self.in_1) and (predecessor not in self.core_1):
  589. num1 += 1
  590. num2 = 0
  591. for predecessor in self.G2.pred[G2_node]:
  592. if (predecessor in self.in_2) and (predecessor not in self.core_2):
  593. num2 += 1
  594. if self.test == "graph":
  595. if num1 != num2:
  596. return False
  597. else: # self.test == 'subgraph'
  598. if not (num1 >= num2):
  599. return False
  600. # The number of successors of n that are in T_1^{in} is equal to the
  601. # number of successors of m that are in T_2^{in}.
  602. num1 = 0
  603. for successor in self.G1[G1_node]:
  604. if (successor in self.in_1) and (successor not in self.core_1):
  605. num1 += 1
  606. num2 = 0
  607. for successor in self.G2[G2_node]:
  608. if (successor in self.in_2) and (successor not in self.core_2):
  609. num2 += 1
  610. if self.test == "graph":
  611. if num1 != num2:
  612. return False
  613. else: # self.test == 'subgraph'
  614. if not (num1 >= num2):
  615. return False
  616. # R_termout
  617. # The number of predecessors of n that are in T_1^{out} is equal to the
  618. # number of predecessors of m that are in T_2^{out}.
  619. num1 = 0
  620. for predecessor in self.G1.pred[G1_node]:
  621. if (predecessor in self.out_1) and (predecessor not in self.core_1):
  622. num1 += 1
  623. num2 = 0
  624. for predecessor in self.G2.pred[G2_node]:
  625. if (predecessor in self.out_2) and (predecessor not in self.core_2):
  626. num2 += 1
  627. if self.test == "graph":
  628. if num1 != num2:
  629. return False
  630. else: # self.test == 'subgraph'
  631. if not (num1 >= num2):
  632. return False
  633. # The number of successors of n that are in T_1^{out} is equal to the
  634. # number of successors of m that are in T_2^{out}.
  635. num1 = 0
  636. for successor in self.G1[G1_node]:
  637. if (successor in self.out_1) and (successor not in self.core_1):
  638. num1 += 1
  639. num2 = 0
  640. for successor in self.G2[G2_node]:
  641. if (successor in self.out_2) and (successor not in self.core_2):
  642. num2 += 1
  643. if self.test == "graph":
  644. if num1 != num2:
  645. return False
  646. else: # self.test == 'subgraph'
  647. if not (num1 >= num2):
  648. return False
  649. # Look ahead 2
  650. # R_new
  651. # The number of predecessors of n that are neither in the core_1 nor
  652. # T_1^{in} nor T_1^{out} is equal to the number of predecessors of m
  653. # that are neither in core_2 nor T_2^{in} nor T_2^{out}.
  654. num1 = 0
  655. for predecessor in self.G1.pred[G1_node]:
  656. if (predecessor not in self.in_1) and (predecessor not in self.out_1):
  657. num1 += 1
  658. num2 = 0
  659. for predecessor in self.G2.pred[G2_node]:
  660. if (predecessor not in self.in_2) and (predecessor not in self.out_2):
  661. num2 += 1
  662. if self.test == "graph":
  663. if num1 != num2:
  664. return False
  665. else: # self.test == 'subgraph'
  666. if not (num1 >= num2):
  667. return False
  668. # The number of successors of n that are neither in the core_1 nor
  669. # T_1^{in} nor T_1^{out} is equal to the number of successors of m
  670. # that are neither in core_2 nor T_2^{in} nor T_2^{out}.
  671. num1 = 0
  672. for successor in self.G1[G1_node]:
  673. if (successor not in self.in_1) and (successor not in self.out_1):
  674. num1 += 1
  675. num2 = 0
  676. for successor in self.G2[G2_node]:
  677. if (successor not in self.in_2) and (successor not in self.out_2):
  678. num2 += 1
  679. if self.test == "graph":
  680. if num1 != num2:
  681. return False
  682. else: # self.test == 'subgraph'
  683. if not (num1 >= num2):
  684. return False
  685. # Otherwise, this node pair is syntactically feasible!
  686. return True
  687. class GMState:
  688. """Internal representation of state for the GraphMatcher class.
  689. This class is used internally by the GraphMatcher class. It is used
  690. only to store state specific data. There will be at most G2.order() of
  691. these objects in memory at a time, due to the depth-first search
  692. strategy employed by the VF2 algorithm.
  693. """
  694. def __init__(self, GM, G1_node=None, G2_node=None):
  695. """Initializes GMState object.
  696. Pass in the GraphMatcher to which this GMState belongs and the
  697. new node pair that will be added to the GraphMatcher's current
  698. isomorphism mapping.
  699. """
  700. self.GM = GM
  701. # Initialize the last stored node pair.
  702. self.G1_node = None
  703. self.G2_node = None
  704. self.depth = len(GM.core_1)
  705. if G1_node is None or G2_node is None:
  706. # Then we reset the class variables
  707. GM.core_1 = {}
  708. GM.core_2 = {}
  709. GM.inout_1 = {}
  710. GM.inout_2 = {}
  711. # Watch out! G1_node == 0 should evaluate to True.
  712. if G1_node is not None and G2_node is not None:
  713. # Add the node pair to the isomorphism mapping.
  714. GM.core_1[G1_node] = G2_node
  715. GM.core_2[G2_node] = G1_node
  716. # Store the node that was added last.
  717. self.G1_node = G1_node
  718. self.G2_node = G2_node
  719. # Now we must update the other two vectors.
  720. # We will add only if it is not in there already!
  721. self.depth = len(GM.core_1)
  722. # First we add the new nodes...
  723. if G1_node not in GM.inout_1:
  724. GM.inout_1[G1_node] = self.depth
  725. if G2_node not in GM.inout_2:
  726. GM.inout_2[G2_node] = self.depth
  727. # Now we add every other node...
  728. # Updates for T_1^{inout}
  729. new_nodes = set()
  730. for node in GM.core_1:
  731. new_nodes.update(
  732. [neighbor for neighbor in GM.G1[node] if neighbor not in GM.core_1]
  733. )
  734. for node in new_nodes:
  735. if node not in GM.inout_1:
  736. GM.inout_1[node] = self.depth
  737. # Updates for T_2^{inout}
  738. new_nodes = set()
  739. for node in GM.core_2:
  740. new_nodes.update(
  741. [neighbor for neighbor in GM.G2[node] if neighbor not in GM.core_2]
  742. )
  743. for node in new_nodes:
  744. if node not in GM.inout_2:
  745. GM.inout_2[node] = self.depth
  746. def restore(self):
  747. """Deletes the GMState object and restores the class variables."""
  748. # First we remove the node that was added from the core vectors.
  749. # Watch out! G1_node == 0 should evaluate to True.
  750. if self.G1_node is not None and self.G2_node is not None:
  751. del self.GM.core_1[self.G1_node]
  752. del self.GM.core_2[self.G2_node]
  753. # Now we revert the other two vectors.
  754. # Thus, we delete all entries which have this depth level.
  755. for vector in (self.GM.inout_1, self.GM.inout_2):
  756. for node in list(vector.keys()):
  757. if vector[node] == self.depth:
  758. del vector[node]
  759. class DiGMState:
  760. """Internal representation of state for the DiGraphMatcher class.
  761. This class is used internally by the DiGraphMatcher class. It is used
  762. only to store state specific data. There will be at most G2.order() of
  763. these objects in memory at a time, due to the depth-first search
  764. strategy employed by the VF2 algorithm.
  765. """
  766. def __init__(self, GM, G1_node=None, G2_node=None):
  767. """Initializes DiGMState object.
  768. Pass in the DiGraphMatcher to which this DiGMState belongs and the
  769. new node pair that will be added to the GraphMatcher's current
  770. isomorphism mapping.
  771. """
  772. self.GM = GM
  773. # Initialize the last stored node pair.
  774. self.G1_node = None
  775. self.G2_node = None
  776. self.depth = len(GM.core_1)
  777. if G1_node is None or G2_node is None:
  778. # Then we reset the class variables
  779. GM.core_1 = {}
  780. GM.core_2 = {}
  781. GM.in_1 = {}
  782. GM.in_2 = {}
  783. GM.out_1 = {}
  784. GM.out_2 = {}
  785. # Watch out! G1_node == 0 should evaluate to True.
  786. if G1_node is not None and G2_node is not None:
  787. # Add the node pair to the isomorphism mapping.
  788. GM.core_1[G1_node] = G2_node
  789. GM.core_2[G2_node] = G1_node
  790. # Store the node that was added last.
  791. self.G1_node = G1_node
  792. self.G2_node = G2_node
  793. # Now we must update the other four vectors.
  794. # We will add only if it is not in there already!
  795. self.depth = len(GM.core_1)
  796. # First we add the new nodes...
  797. for vector in (GM.in_1, GM.out_1):
  798. if G1_node not in vector:
  799. vector[G1_node] = self.depth
  800. for vector in (GM.in_2, GM.out_2):
  801. if G2_node not in vector:
  802. vector[G2_node] = self.depth
  803. # Now we add every other node...
  804. # Updates for T_1^{in}
  805. new_nodes = set()
  806. for node in GM.core_1:
  807. new_nodes.update(
  808. [
  809. predecessor
  810. for predecessor in GM.G1.predecessors(node)
  811. if predecessor not in GM.core_1
  812. ]
  813. )
  814. for node in new_nodes:
  815. if node not in GM.in_1:
  816. GM.in_1[node] = self.depth
  817. # Updates for T_2^{in}
  818. new_nodes = set()
  819. for node in GM.core_2:
  820. new_nodes.update(
  821. [
  822. predecessor
  823. for predecessor in GM.G2.predecessors(node)
  824. if predecessor not in GM.core_2
  825. ]
  826. )
  827. for node in new_nodes:
  828. if node not in GM.in_2:
  829. GM.in_2[node] = self.depth
  830. # Updates for T_1^{out}
  831. new_nodes = set()
  832. for node in GM.core_1:
  833. new_nodes.update(
  834. [
  835. successor
  836. for successor in GM.G1.successors(node)
  837. if successor not in GM.core_1
  838. ]
  839. )
  840. for node in new_nodes:
  841. if node not in GM.out_1:
  842. GM.out_1[node] = self.depth
  843. # Updates for T_2^{out}
  844. new_nodes = set()
  845. for node in GM.core_2:
  846. new_nodes.update(
  847. [
  848. successor
  849. for successor in GM.G2.successors(node)
  850. if successor not in GM.core_2
  851. ]
  852. )
  853. for node in new_nodes:
  854. if node not in GM.out_2:
  855. GM.out_2[node] = self.depth
  856. def restore(self):
  857. """Deletes the DiGMState object and restores the class variables."""
  858. # First we remove the node that was added from the core vectors.
  859. # Watch out! G1_node == 0 should evaluate to True.
  860. if self.G1_node is not None and self.G2_node is not None:
  861. del self.GM.core_1[self.G1_node]
  862. del self.GM.core_2[self.G2_node]
  863. # Now we revert the other four vectors.
  864. # Thus, we delete all entries which have this depth level.
  865. for vector in (self.GM.in_1, self.GM.in_2, self.GM.out_1, self.GM.out_2):
  866. for node in list(vector.keys()):
  867. if vector[node] == self.depth:
  868. del vector[node]