1"""
2***************
3VF2++ Algorithm
4***************
5
6An implementation of the VF2++ algorithm [1]_ for Graph Isomorphism testing.
7
8The simplest interface to use this module is to call:
9
10`vf2pp_is_isomorphic`: to check whether two graphs are isomorphic.
11`vf2pp_isomorphism`: to obtain the node mapping between two graphs,
12in case they are isomorphic.
13`vf2pp_all_isomorphisms`: to generate all possible mappings between two graphs,
14if isomorphic.
15
16Introduction
17------------
18The VF2++ algorithm, follows a similar logic to that of VF2, while also
19introducing new easy-to-check cutting rules and determining the optimal access
20order of nodes. It is also implemented in a non-recursive manner, which saves
21both time and space, when compared to its previous counterpart.
22
23The optimal node ordering is obtained after taking into consideration both the
24degree but also the label rarity of each node.
25This way we place the nodes that are more likely to match, first in the order,
26thus examining the most promising branches in the beginning.
27The rules also consider node labels, making it easier to prune unfruitful
28branches early in the process.
29
30Examples
31--------
32
33Suppose G1 and G2 are Isomorphic Graphs. Verification is as follows:
34
35Without node labels:
36
37>>> import networkx as nx
38>>> G1 = nx.path_graph(4)
39>>> G2 = nx.path_graph(4)
40>>> nx.vf2pp_is_isomorphic(G1, G2, node_label=None)
41True
42>>> nx.vf2pp_isomorphism(G1, G2, node_label=None)
43{1: 1, 2: 2, 0: 0, 3: 3}
44
45With node labels:
46
47>>> G1 = nx.path_graph(4)
48>>> G2 = nx.path_graph(4)
49>>> mapped = {1: 1, 2: 2, 3: 3, 0: 0}
50>>> nx.set_node_attributes(
51... G1, dict(zip(G1, ["blue", "red", "green", "yellow"])), "label"
52... )
53>>> nx.set_node_attributes(
54... G2,
55... dict(zip([mapped[u] for u in G1], ["blue", "red", "green", "yellow"])),
56... "label",
57... )
58>>> nx.vf2pp_is_isomorphic(G1, G2, node_label="label")
59True
60>>> nx.vf2pp_isomorphism(G1, G2, node_label="label")
61{1: 1, 2: 2, 0: 0, 3: 3}
62
63References
64----------
65.. [1] Jüttner, Alpár & Madarasi, Péter. (2018). "VF2++—An improved subgraph
66 isomorphism algorithm". Discrete Applied Mathematics. 242.
67 https://doi.org/10.1016/j.dam.2018.02.018
68
69"""
70
71import collections
72
73import networkx as nx
74
75__all__ = ["vf2pp_isomorphism", "vf2pp_is_isomorphic", "vf2pp_all_isomorphisms"]
76
77_GraphParameters = collections.namedtuple(
78 "_GraphParameters",
79 [
80 "G1",
81 "G2",
82 "G1_labels",
83 "G2_labels",
84 "nodes_of_G1Labels",
85 "nodes_of_G2Labels",
86 "G2_nodes_of_degree",
87 ],
88)
89
90_StateParameters = collections.namedtuple(
91 "_StateParameters",
92 [
93 "mapping",
94 "reverse_mapping",
95 "T1",
96 "T1_in",
97 "T1_tilde",
98 "T1_tilde_in",
99 "T2",
100 "T2_in",
101 "T2_tilde",
102 "T2_tilde_in",
103 ],
104)
105
106
107@nx._dispatchable(graphs={"G1": 0, "G2": 1}, node_attrs={"node_label": "default_label"})
108def vf2pp_isomorphism(G1, G2, node_label=None, default_label=None):
109 """Return an isomorphic mapping between `G1` and `G2` if it exists.
110
111 Parameters
112 ----------
113 G1, G2 : NetworkX Graph or MultiGraph instances.
114 The two graphs to check for isomorphism.
115
116 node_label : str, optional
117 The name of the node attribute to be used when comparing nodes.
118 The default is `None`, meaning node attributes are not considered
119 in the comparison. Any node that doesn't have the `node_label`
120 attribute uses `default_label` instead.
121
122 default_label : scalar
123 Default value to use when a node doesn't have an attribute
124 named `node_label`. Default is `None`.
125
126 Returns
127 -------
128 dict or None
129 Node mapping if the two graphs are isomorphic. None otherwise.
130 """
131 try:
132 mapping = next(vf2pp_all_isomorphisms(G1, G2, node_label, default_label))
133 return mapping
134 except StopIteration:
135 return None
136
137
138@nx._dispatchable(graphs={"G1": 0, "G2": 1}, node_attrs={"node_label": "default_label"})
139def vf2pp_is_isomorphic(G1, G2, node_label=None, default_label=None):
140 """Examines whether G1 and G2 are isomorphic.
141
142 Parameters
143 ----------
144 G1, G2 : NetworkX Graph or MultiGraph instances.
145 The two graphs to check for isomorphism.
146
147 node_label : str, optional
148 The name of the node attribute to be used when comparing nodes.
149 The default is `None`, meaning node attributes are not considered
150 in the comparison. Any node that doesn't have the `node_label`
151 attribute uses `default_label` instead.
152
153 default_label : scalar
154 Default value to use when a node doesn't have an attribute
155 named `node_label`. Default is `None`.
156
157 Returns
158 -------
159 bool
160 True if the two graphs are isomorphic, False otherwise.
161 """
162 if vf2pp_isomorphism(G1, G2, node_label, default_label) is not None:
163 return True
164 return False
165
166
167@nx._dispatchable(graphs={"G1": 0, "G2": 1}, node_attrs={"node_label": "default_label"})
168def vf2pp_all_isomorphisms(G1, G2, node_label=None, default_label=None):
169 """Yields all the possible mappings between G1 and G2.
170
171 Parameters
172 ----------
173 G1, G2 : NetworkX Graph or MultiGraph instances.
174 The two graphs to check for isomorphism.
175
176 node_label : str, optional
177 The name of the node attribute to be used when comparing nodes.
178 The default is `None`, meaning node attributes are not considered
179 in the comparison. Any node that doesn't have the `node_label`
180 attribute uses `default_label` instead.
181
182 default_label : scalar
183 Default value to use when a node doesn't have an attribute
184 named `node_label`. Default is `None`.
185
186 Yields
187 ------
188 dict
189 Isomorphic mapping between the nodes in `G1` and `G2`.
190 """
191 if G1.number_of_nodes() == 0 or G2.number_of_nodes() == 0:
192 return False
193
194 # Create the degree dicts based on graph type
195 if G1.is_directed():
196 G1_degree = {
197 n: (in_degree, out_degree)
198 for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
199 }
200 G2_degree = {
201 n: (in_degree, out_degree)
202 for (n, in_degree), (_, out_degree) in zip(G2.in_degree, G2.out_degree)
203 }
204 else:
205 G1_degree = dict(G1.degree)
206 G2_degree = dict(G2.degree)
207
208 if not G1.is_directed():
209 find_candidates = _find_candidates
210 restore_Tinout = _restore_Tinout
211 else:
212 find_candidates = _find_candidates_Di
213 restore_Tinout = _restore_Tinout_Di
214
215 # Check that both graphs have the same number of nodes and degree sequence
216 if G1.order() != G2.order():
217 return False
218 if sorted(G1_degree.values()) != sorted(G2_degree.values()):
219 return False
220
221 # Initialize parameters and cache necessary information about degree and labels
222 graph_params, state_params = _initialize_parameters(
223 G1, G2, G2_degree, node_label, default_label
224 )
225
226 # Check if G1 and G2 have the same labels, and that number of nodes per label
227 # is equal between the two graphs
228 if not _precheck_label_properties(graph_params):
229 return False
230
231 # Calculate the optimal node ordering
232 node_order = _matching_order(graph_params)
233
234 # Initialize the stack
235 stack = []
236 candidates = iter(
237 find_candidates(node_order[0], graph_params, state_params, G1_degree)
238 )
239 stack.append((node_order[0], candidates))
240
241 mapping = state_params.mapping
242 reverse_mapping = state_params.reverse_mapping
243
244 # Index of the node from the order, currently being examined
245 matching_node = 1
246
247 while stack:
248 current_node, candidate_nodes = stack[-1]
249
250 try:
251 candidate = next(candidate_nodes)
252 except StopIteration:
253 # If no remaining candidates, return to a previous state, and follow another branch
254 stack.pop()
255 matching_node -= 1
256 if stack:
257 # Pop the previously added u-v pair, and look for a different candidate _v for u
258 popped_node1, _ = stack[-1]
259 popped_node2 = mapping[popped_node1]
260 mapping.pop(popped_node1)
261 reverse_mapping.pop(popped_node2)
262 restore_Tinout(popped_node1, popped_node2, graph_params, state_params)
263 continue
264
265 if _feasibility(current_node, candidate, graph_params, state_params):
266 # Terminate if mapping is extended to its full
267 if len(mapping) == G2.number_of_nodes() - 1:
268 cp_mapping = mapping.copy()
269 cp_mapping[current_node] = candidate
270 yield cp_mapping
271 continue
272
273 # Feasibility rules pass, so extend the mapping and update the parameters
274 mapping[current_node] = candidate
275 reverse_mapping[candidate] = current_node
276 _update_Tinout(current_node, candidate, graph_params, state_params)
277 # Append the next node and its candidates to the stack
278 candidates = iter(
279 find_candidates(
280 node_order[matching_node], graph_params, state_params, G1_degree
281 )
282 )
283 stack.append((node_order[matching_node], candidates))
284 matching_node += 1
285
286
287def _precheck_label_properties(graph_params):
288 G1, G2, G1_labels, G2_labels, nodes_of_G1Labels, nodes_of_G2Labels, _ = graph_params
289 if any(
290 label not in nodes_of_G1Labels or len(nodes_of_G1Labels[label]) != len(nodes)
291 for label, nodes in nodes_of_G2Labels.items()
292 ):
293 return False
294 return True
295
296
297def _initialize_parameters(G1, G2, G2_degree, node_label=None, default_label=-1):
298 """Initializes all the necessary parameters for VF2++
299
300 Parameters
301 ----------
302 G1,G2: NetworkX Graph or MultiGraph instances.
303 The two graphs to check for isomorphism or monomorphism
304
305 G1_labels,G2_labels: dict
306 The label of every node in G1 and G2 respectively
307
308 Returns
309 -------
310 graph_params: namedtuple
311 Contains all the Graph-related parameters:
312
313 G1,G2
314 G1_labels,G2_labels: dict
315
316 state_params: namedtuple
317 Contains all the State-related parameters:
318
319 mapping: dict
320 The mapping as extended so far. Maps nodes of G1 to nodes of G2
321
322 reverse_mapping: dict
323 The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1.
324 It's basically "mapping" reversed
325
326 T1, T2: set
327 Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
328 that are not in the mapping, but are neighbors of nodes that are.
329
330 T1_out, T2_out: set
331 Ti_out contains all the nodes from Gi, that are neither in the mapping
332 nor in Ti
333 """
334 G1_labels = dict(G1.nodes(data=node_label, default=default_label))
335 G2_labels = dict(G2.nodes(data=node_label, default=default_label))
336
337 graph_params = _GraphParameters(
338 G1,
339 G2,
340 G1_labels,
341 G2_labels,
342 nx.utils.groups(G1_labels),
343 nx.utils.groups(G2_labels),
344 nx.utils.groups(G2_degree),
345 )
346
347 T1, T1_in = set(), set()
348 T2, T2_in = set(), set()
349 if G1.is_directed():
350 T1_tilde, T1_tilde_in = (
351 set(G1.nodes()),
352 set(),
353 ) # todo: do we need Ti_tilde_in? What nodes does it have?
354 T2_tilde, T2_tilde_in = set(G2.nodes()), set()
355 else:
356 T1_tilde, T1_tilde_in = set(G1.nodes()), set()
357 T2_tilde, T2_tilde_in = set(G2.nodes()), set()
358
359 state_params = _StateParameters(
360 {},
361 {},
362 T1,
363 T1_in,
364 T1_tilde,
365 T1_tilde_in,
366 T2,
367 T2_in,
368 T2_tilde,
369 T2_tilde_in,
370 )
371
372 return graph_params, state_params
373
374
375def _matching_order(graph_params):
376 """The node ordering as introduced in VF2++.
377
378 Notes
379 -----
380 Taking into account the structure of the Graph and the node labeling, the
381 nodes are placed in an order such that, most of the unfruitful/infeasible
382 branches of the search space can be pruned on high levels, significantly
383 decreasing the number of visited states. The premise is that, the algorithm
384 will be able to recognize inconsistencies early, proceeding to go deep into
385 the search tree only if it's needed.
386
387 Parameters
388 ----------
389 graph_params: namedtuple
390 Contains:
391
392 G1,G2: NetworkX Graph or MultiGraph instances.
393 The two graphs to check for isomorphism or monomorphism.
394
395 G1_labels,G2_labels: dict
396 The label of every node in G1 and G2 respectively.
397
398 Returns
399 -------
400 node_order: list
401 The ordering of the nodes.
402 """
403 G1, G2, G1_labels, _, _, nodes_of_G2Labels, _ = graph_params
404 if not G1 and not G2:
405 return {}
406
407 if G1.is_directed():
408 G1 = G1.to_undirected(as_view=True)
409
410 V1_unordered = set(G1.nodes())
411 label_rarity = {label: len(nodes) for label, nodes in nodes_of_G2Labels.items()}
412 used_degrees = {node: 0 for node in G1}
413 node_order = []
414
415 while V1_unordered:
416 max_rarity = min(label_rarity[G1_labels[x]] for x in V1_unordered)
417 rarest_nodes = [
418 n for n in V1_unordered if label_rarity[G1_labels[n]] == max_rarity
419 ]
420 max_node = max(rarest_nodes, key=G1.degree)
421
422 for dlevel_nodes in nx.bfs_layers(G1, max_node):
423 nodes_to_add = dlevel_nodes.copy()
424 while nodes_to_add:
425 max_used_degree = max(used_degrees[n] for n in nodes_to_add)
426 max_used_degree_nodes = [
427 n for n in nodes_to_add if used_degrees[n] == max_used_degree
428 ]
429 max_degree = max(G1.degree[n] for n in max_used_degree_nodes)
430 max_degree_nodes = [
431 n for n in max_used_degree_nodes if G1.degree[n] == max_degree
432 ]
433 next_node = min(
434 max_degree_nodes, key=lambda x: label_rarity[G1_labels[x]]
435 )
436
437 node_order.append(next_node)
438 for node in G1.neighbors(next_node):
439 used_degrees[node] += 1
440
441 nodes_to_add.remove(next_node)
442 label_rarity[G1_labels[next_node]] -= 1
443 V1_unordered.discard(next_node)
444
445 return node_order
446
447
448def _find_candidates(
449 u, graph_params, state_params, G1_degree
450): # todo: make the 4th argument the degree of u
451 """Given node u of G1, finds the candidates of u from G2.
452
453 Parameters
454 ----------
455 u: Graph node
456 The node from G1 for which to find the candidates from G2.
457
458 graph_params: namedtuple
459 Contains all the Graph-related parameters:
460
461 G1,G2: NetworkX Graph or MultiGraph instances.
462 The two graphs to check for isomorphism or monomorphism
463
464 G1_labels,G2_labels: dict
465 The label of every node in G1 and G2 respectively
466
467 state_params: namedtuple
468 Contains all the State-related parameters:
469
470 mapping: dict
471 The mapping as extended so far. Maps nodes of G1 to nodes of G2
472
473 reverse_mapping: dict
474 The reverse mapping as extended so far. Maps nodes from G2 to nodes
475 of G1. It's basically "mapping" reversed
476
477 T1, T2: set
478 Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
479 that are not in the mapping, but are neighbors of nodes that are.
480
481 T1_tilde, T2_tilde: set
482 Ti_tilde contains all the nodes from Gi, that are neither in the
483 mapping nor in Ti
484
485 Returns
486 -------
487 candidates: set
488 The nodes from G2 which are candidates for u.
489 """
490 G1, G2, G1_labels, _, _, nodes_of_G2Labels, G2_nodes_of_degree = graph_params
491 mapping, reverse_mapping, _, _, _, _, _, _, T2_tilde, _ = state_params
492
493 covered_nbrs = [nbr for nbr in G1[u] if nbr in mapping]
494 if not covered_nbrs:
495 candidates = set(nodes_of_G2Labels[G1_labels[u]])
496 candidates.intersection_update(G2_nodes_of_degree[G1_degree[u]])
497 candidates.intersection_update(T2_tilde)
498 candidates.difference_update(reverse_mapping)
499 if G1.is_multigraph():
500 candidates.difference_update(
501 {
502 node
503 for node in candidates
504 if G1.number_of_edges(u, u) != G2.number_of_edges(node, node)
505 }
506 )
507 return candidates
508
509 nbr1 = covered_nbrs[0]
510 common_nodes = set(G2[mapping[nbr1]])
511
512 for nbr1 in covered_nbrs[1:]:
513 common_nodes.intersection_update(G2[mapping[nbr1]])
514
515 common_nodes.difference_update(reverse_mapping)
516 common_nodes.intersection_update(G2_nodes_of_degree[G1_degree[u]])
517 common_nodes.intersection_update(nodes_of_G2Labels[G1_labels[u]])
518 if G1.is_multigraph():
519 common_nodes.difference_update(
520 {
521 node
522 for node in common_nodes
523 if G1.number_of_edges(u, u) != G2.number_of_edges(node, node)
524 }
525 )
526 return common_nodes
527
528
529def _find_candidates_Di(u, graph_params, state_params, G1_degree):
530 G1, G2, G1_labels, _, _, nodes_of_G2Labels, G2_nodes_of_degree = graph_params
531 mapping, reverse_mapping, _, _, _, _, _, _, T2_tilde, _ = state_params
532
533 covered_successors = [succ for succ in G1[u] if succ in mapping]
534 covered_predecessors = [pred for pred in G1.pred[u] if pred in mapping]
535
536 if not (covered_successors or covered_predecessors):
537 candidates = set(nodes_of_G2Labels[G1_labels[u]])
538 candidates.intersection_update(G2_nodes_of_degree[G1_degree[u]])
539 candidates.intersection_update(T2_tilde)
540 candidates.difference_update(reverse_mapping)
541 if G1.is_multigraph():
542 candidates.difference_update(
543 {
544 node
545 for node in candidates
546 if G1.number_of_edges(u, u) != G2.number_of_edges(node, node)
547 }
548 )
549 return candidates
550
551 if covered_successors:
552 succ1 = covered_successors[0]
553 common_nodes = set(G2.pred[mapping[succ1]])
554
555 for succ1 in covered_successors[1:]:
556 common_nodes.intersection_update(G2.pred[mapping[succ1]])
557 else:
558 pred1 = covered_predecessors.pop()
559 common_nodes = set(G2[mapping[pred1]])
560
561 for pred1 in covered_predecessors:
562 common_nodes.intersection_update(G2[mapping[pred1]])
563
564 common_nodes.difference_update(reverse_mapping)
565 common_nodes.intersection_update(G2_nodes_of_degree[G1_degree[u]])
566 common_nodes.intersection_update(nodes_of_G2Labels[G1_labels[u]])
567 if G1.is_multigraph():
568 common_nodes.difference_update(
569 {
570 node
571 for node in common_nodes
572 if G1.number_of_edges(u, u) != G2.number_of_edges(node, node)
573 }
574 )
575 return common_nodes
576
577
578def _feasibility(node1, node2, graph_params, state_params):
579 """Given a candidate pair of nodes u and v from G1 and G2 respectively,
580 checks if it's feasible to extend the mapping, i.e. if u and v can be matched.
581
582 Notes
583 -----
584 This function performs all the necessary checking by applying both consistency
585 and cutting rules.
586
587 Parameters
588 ----------
589 node1, node2: Graph node
590 The candidate pair of nodes being checked for matching
591
592 graph_params: namedtuple
593 Contains all the Graph-related parameters:
594
595 G1,G2: NetworkX Graph or MultiGraph instances.
596 The two graphs to check for isomorphism or monomorphism
597
598 G1_labels,G2_labels: dict
599 The label of every node in G1 and G2 respectively
600
601 state_params: namedtuple
602 Contains all the State-related parameters:
603
604 mapping: dict
605 The mapping as extended so far. Maps nodes of G1 to nodes of G2
606
607 reverse_mapping: dict
608 The reverse mapping as extended so far. Maps nodes from G2 to nodes
609 of G1. It's basically "mapping" reversed
610
611 T1, T2: set
612 Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
613 that are not in the mapping, but are neighbors of nodes that are.
614
615 T1_out, T2_out: set
616 Ti_out contains all the nodes from Gi, that are neither in the mapping
617 nor in Ti
618
619 Returns
620 -------
621 True if all checks are successful, False otherwise.
622 """
623 G1 = graph_params.G1
624
625 if _cut_PT(node1, node2, graph_params, state_params):
626 return False
627
628 if G1.is_multigraph():
629 if not _consistent_PT(node1, node2, graph_params, state_params):
630 return False
631
632 return True
633
634
635def _cut_PT(u, v, graph_params, state_params):
636 """Implements the cutting rules for the ISO problem.
637
638 Parameters
639 ----------
640 u, v: Graph node
641 The two candidate nodes being examined.
642
643 graph_params: namedtuple
644 Contains all the Graph-related parameters:
645
646 G1,G2: NetworkX Graph or MultiGraph instances.
647 The two graphs to check for isomorphism or monomorphism
648
649 G1_labels,G2_labels: dict
650 The label of every node in G1 and G2 respectively
651
652 state_params: namedtuple
653 Contains all the State-related parameters:
654
655 mapping: dict
656 The mapping as extended so far. Maps nodes of G1 to nodes of G2
657
658 reverse_mapping: dict
659 The reverse mapping as extended so far. Maps nodes from G2 to nodes
660 of G1. It's basically "mapping" reversed
661
662 T1, T2: set
663 Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
664 that are not in the mapping, but are neighbors of nodes that are.
665
666 T1_tilde, T2_tilde: set
667 Ti_out contains all the nodes from Gi, that are neither in the
668 mapping nor in Ti
669
670 Returns
671 -------
672 True if we should prune this branch, i.e. the node pair failed the cutting checks. False otherwise.
673 """
674 G1, G2, G1_labels, G2_labels, _, _, _ = graph_params
675 (
676 _,
677 _,
678 T1,
679 T1_in,
680 T1_tilde,
681 _,
682 T2,
683 T2_in,
684 T2_tilde,
685 _,
686 ) = state_params
687
688 u_labels_predecessors, v_labels_predecessors = {}, {}
689 if G1.is_directed():
690 u_labels_predecessors = nx.utils.groups(
691 {n1: G1_labels[n1] for n1 in G1.pred[u]}
692 )
693 v_labels_predecessors = nx.utils.groups(
694 {n2: G2_labels[n2] for n2 in G2.pred[v]}
695 )
696
697 if set(u_labels_predecessors.keys()) != set(v_labels_predecessors.keys()):
698 return True
699
700 u_labels_successors = nx.utils.groups({n1: G1_labels[n1] for n1 in G1[u]})
701 v_labels_successors = nx.utils.groups({n2: G2_labels[n2] for n2 in G2[v]})
702
703 # if the neighbors of u, do not have the same labels as those of v, NOT feasible.
704 if set(u_labels_successors.keys()) != set(v_labels_successors.keys()):
705 return True
706
707 for label, G1_nbh in u_labels_successors.items():
708 G2_nbh = v_labels_successors[label]
709
710 if G1.is_multigraph():
711 # Check for every neighbor in the neighborhood, if u-nbr1 has same edges as v-nbr2
712 u_nbrs_edges = sorted(G1.number_of_edges(u, x) for x in G1_nbh)
713 v_nbrs_edges = sorted(G2.number_of_edges(v, x) for x in G2_nbh)
714 if any(
715 u_nbr_edges != v_nbr_edges
716 for u_nbr_edges, v_nbr_edges in zip(u_nbrs_edges, v_nbrs_edges)
717 ):
718 return True
719
720 if len(T1.intersection(G1_nbh)) != len(T2.intersection(G2_nbh)):
721 return True
722 if len(T1_tilde.intersection(G1_nbh)) != len(T2_tilde.intersection(G2_nbh)):
723 return True
724 if G1.is_directed() and len(T1_in.intersection(G1_nbh)) != len(
725 T2_in.intersection(G2_nbh)
726 ):
727 return True
728
729 if not G1.is_directed():
730 return False
731
732 for label, G1_pred in u_labels_predecessors.items():
733 G2_pred = v_labels_predecessors[label]
734
735 if G1.is_multigraph():
736 # Check for every neighbor in the neighborhood, if u-nbr1 has same edges as v-nbr2
737 u_pred_edges = sorted(G1.number_of_edges(u, x) for x in G1_pred)
738 v_pred_edges = sorted(G2.number_of_edges(v, x) for x in G2_pred)
739 if any(
740 u_nbr_edges != v_nbr_edges
741 for u_nbr_edges, v_nbr_edges in zip(u_pred_edges, v_pred_edges)
742 ):
743 return True
744
745 if len(T1.intersection(G1_pred)) != len(T2.intersection(G2_pred)):
746 return True
747 if len(T1_tilde.intersection(G1_pred)) != len(T2_tilde.intersection(G2_pred)):
748 return True
749 if len(T1_in.intersection(G1_pred)) != len(T2_in.intersection(G2_pred)):
750 return True
751
752 return False
753
754
755def _consistent_PT(u, v, graph_params, state_params):
756 """Checks the consistency of extending the mapping using the current node pair.
757
758 Parameters
759 ----------
760 u, v: Graph node
761 The two candidate nodes being examined.
762
763 graph_params: namedtuple
764 Contains all the Graph-related parameters:
765
766 G1,G2: NetworkX Graph or MultiGraph instances.
767 The two graphs to check for isomorphism or monomorphism
768
769 G1_labels,G2_labels: dict
770 The label of every node in G1 and G2 respectively
771
772 state_params: namedtuple
773 Contains all the State-related parameters:
774
775 mapping: dict
776 The mapping as extended so far. Maps nodes of G1 to nodes of G2
777
778 reverse_mapping: dict
779 The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1.
780 It's basically "mapping" reversed
781
782 T1, T2: set
783 Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
784 that are not in the mapping, but are neighbors of nodes that are.
785
786 T1_out, T2_out: set
787 Ti_out contains all the nodes from Gi, that are neither in the mapping
788 nor in Ti
789
790 Returns
791 -------
792 True if the pair passes all the consistency checks successfully. False otherwise.
793 """
794 G1, G2 = graph_params.G1, graph_params.G2
795 mapping, reverse_mapping = state_params.mapping, state_params.reverse_mapping
796
797 for neighbor in G1[u]:
798 if neighbor in mapping:
799 if G1.number_of_edges(u, neighbor) != G2.number_of_edges(
800 v, mapping[neighbor]
801 ):
802 return False
803
804 for neighbor in G2[v]:
805 if neighbor in reverse_mapping:
806 if G1.number_of_edges(u, reverse_mapping[neighbor]) != G2.number_of_edges(
807 v, neighbor
808 ):
809 return False
810
811 if not G1.is_directed():
812 return True
813
814 for predecessor in G1.pred[u]:
815 if predecessor in mapping:
816 if G1.number_of_edges(predecessor, u) != G2.number_of_edges(
817 mapping[predecessor], v
818 ):
819 return False
820
821 for predecessor in G2.pred[v]:
822 if predecessor in reverse_mapping:
823 if G1.number_of_edges(
824 reverse_mapping[predecessor], u
825 ) != G2.number_of_edges(predecessor, v):
826 return False
827
828 return True
829
830
831def _update_Tinout(new_node1, new_node2, graph_params, state_params):
832 """Updates the Ti/Ti_out (i=1,2) when a new node pair u-v is added to the mapping.
833
834 Notes
835 -----
836 This function should be called right after the feasibility checks are passed,
837 and node1 is mapped to node2. The purpose of this function is to avoid brute
838 force computing of Ti/Ti_out by iterating over all nodes of the graph and
839 checking which nodes satisfy the necessary conditions. Instead, in every step
840 of the algorithm we focus exclusively on the two nodes that are being added
841 to the mapping, incrementally updating Ti/Ti_out.
842
843 Parameters
844 ----------
845 new_node1, new_node2: Graph node
846 The two new nodes, added to the mapping.
847
848 graph_params: namedtuple
849 Contains all the Graph-related parameters:
850
851 G1,G2: NetworkX Graph or MultiGraph instances.
852 The two graphs to check for isomorphism or monomorphism
853
854 G1_labels,G2_labels: dict
855 The label of every node in G1 and G2 respectively
856
857 state_params: namedtuple
858 Contains all the State-related parameters:
859
860 mapping: dict
861 The mapping as extended so far. Maps nodes of G1 to nodes of G2
862
863 reverse_mapping: dict
864 The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1.
865 It's basically "mapping" reversed
866
867 T1, T2: set
868 Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
869 that are not in the mapping, but are neighbors of nodes that are.
870
871 T1_tilde, T2_tilde: set
872 Ti_out contains all the nodes from Gi, that are neither in the mapping nor in Ti
873 """
874 G1, G2, _, _, _, _, _ = graph_params
875 (
876 mapping,
877 reverse_mapping,
878 T1,
879 T1_in,
880 T1_tilde,
881 T1_tilde_in,
882 T2,
883 T2_in,
884 T2_tilde,
885 T2_tilde_in,
886 ) = state_params
887
888 uncovered_successors_G1 = {succ for succ in G1[new_node1] if succ not in mapping}
889 uncovered_successors_G2 = {
890 succ for succ in G2[new_node2] if succ not in reverse_mapping
891 }
892
893 # Add the uncovered neighbors of node1 and node2 in T1 and T2 respectively
894 T1.update(uncovered_successors_G1)
895 T2.update(uncovered_successors_G2)
896 T1.discard(new_node1)
897 T2.discard(new_node2)
898
899 T1_tilde.difference_update(uncovered_successors_G1)
900 T2_tilde.difference_update(uncovered_successors_G2)
901 T1_tilde.discard(new_node1)
902 T2_tilde.discard(new_node2)
903
904 if not G1.is_directed():
905 return
906
907 uncovered_predecessors_G1 = {
908 pred for pred in G1.pred[new_node1] if pred not in mapping
909 }
910 uncovered_predecessors_G2 = {
911 pred for pred in G2.pred[new_node2] if pred not in reverse_mapping
912 }
913
914 T1_in.update(uncovered_predecessors_G1)
915 T2_in.update(uncovered_predecessors_G2)
916 T1_in.discard(new_node1)
917 T2_in.discard(new_node2)
918
919 T1_tilde.difference_update(uncovered_predecessors_G1)
920 T2_tilde.difference_update(uncovered_predecessors_G2)
921 T1_tilde.discard(new_node1)
922 T2_tilde.discard(new_node2)
923
924
925def _restore_Tinout(popped_node1, popped_node2, graph_params, state_params):
926 """Restores the previous version of Ti/Ti_out when a node pair is deleted
927 from the mapping.
928
929 Parameters
930 ----------
931 popped_node1, popped_node2: Graph node
932 The two nodes deleted from the mapping.
933
934 graph_params: namedtuple
935 Contains all the Graph-related parameters:
936
937 G1,G2: NetworkX Graph or MultiGraph instances.
938 The two graphs to check for isomorphism or monomorphism
939
940 G1_labels,G2_labels: dict
941 The label of every node in G1 and G2 respectively
942
943 state_params: namedtuple
944 Contains all the State-related parameters:
945
946 mapping: dict
947 The mapping as extended so far. Maps nodes of G1 to nodes of G2
948
949 reverse_mapping: dict
950 The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1.
951 It's basically "mapping" reversed
952
953 T1, T2: set
954 Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes
955 that are not in the mapping, but are neighbors of nodes that are.
956
957 T1_tilde, T2_tilde: set
958 Ti_out contains all the nodes from Gi, that are neither in the mapping
959 nor in Ti
960 """
961 # If the node we want to remove from the mapping, has at least one covered
962 # neighbor, add it to T1.
963 G1, G2, _, _, _, _, _ = graph_params
964 (
965 mapping,
966 reverse_mapping,
967 T1,
968 T1_in,
969 T1_tilde,
970 T1_tilde_in,
971 T2,
972 T2_in,
973 T2_tilde,
974 T2_tilde_in,
975 ) = state_params
976
977 is_added = False
978 for neighbor in G1[popped_node1]:
979 if neighbor in mapping:
980 # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1
981 is_added = True
982 T1.add(popped_node1)
983 else:
984 # check if its neighbor has another connection with a covered node.
985 # If not, only then exclude it from T1
986 if any(nbr in mapping for nbr in G1[neighbor]):
987 continue
988 T1.discard(neighbor)
989 T1_tilde.add(neighbor)
990
991 # Case where the node is not present in neither the mapping nor T1.
992 # By definition, it should belong to T1_tilde
993 if not is_added:
994 T1_tilde.add(popped_node1)
995
996 is_added = False
997 for neighbor in G2[popped_node2]:
998 if neighbor in reverse_mapping:
999 is_added = True
1000 T2.add(popped_node2)
1001 else:
1002 if any(nbr in reverse_mapping for nbr in G2[neighbor]):
1003 continue
1004 T2.discard(neighbor)
1005 T2_tilde.add(neighbor)
1006
1007 if not is_added:
1008 T2_tilde.add(popped_node2)
1009
1010
1011def _restore_Tinout_Di(popped_node1, popped_node2, graph_params, state_params):
1012 # If the node we want to remove from the mapping, has at least one covered neighbor, add it to T1.
1013 G1, G2, _, _, _, _, _ = graph_params
1014 (
1015 mapping,
1016 reverse_mapping,
1017 T1,
1018 T1_in,
1019 T1_tilde,
1020 T1_tilde_in,
1021 T2,
1022 T2_in,
1023 T2_tilde,
1024 T2_tilde_in,
1025 ) = state_params
1026
1027 is_added = False
1028 for successor in G1[popped_node1]:
1029 if successor in mapping:
1030 # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1
1031 is_added = True
1032 T1_in.add(popped_node1)
1033 else:
1034 # check if its neighbor has another connection with a covered node.
1035 # If not, only then exclude it from T1
1036 if not any(pred in mapping for pred in G1.pred[successor]):
1037 T1.discard(successor)
1038
1039 if not any(succ in mapping for succ in G1[successor]):
1040 T1_in.discard(successor)
1041
1042 if successor not in T1:
1043 if successor not in T1_in:
1044 T1_tilde.add(successor)
1045
1046 for predecessor in G1.pred[popped_node1]:
1047 if predecessor in mapping:
1048 # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1
1049 is_added = True
1050 T1.add(popped_node1)
1051 else:
1052 # check if its neighbor has another connection with a covered node.
1053 # If not, only then exclude it from T1
1054 if not any(pred in mapping for pred in G1.pred[predecessor]):
1055 T1.discard(predecessor)
1056
1057 if not any(succ in mapping for succ in G1[predecessor]):
1058 T1_in.discard(predecessor)
1059
1060 if not (predecessor in T1 or predecessor in T1_in):
1061 T1_tilde.add(predecessor)
1062
1063 # Case where the node is not present in neither the mapping nor T1.
1064 # By definition it should belong to T1_tilde
1065 if not is_added:
1066 T1_tilde.add(popped_node1)
1067
1068 is_added = False
1069 for successor in G2[popped_node2]:
1070 if successor in reverse_mapping:
1071 is_added = True
1072 T2_in.add(popped_node2)
1073 else:
1074 if not any(pred in reverse_mapping for pred in G2.pred[successor]):
1075 T2.discard(successor)
1076
1077 if not any(succ in reverse_mapping for succ in G2[successor]):
1078 T2_in.discard(successor)
1079
1080 if successor not in T2:
1081 if successor not in T2_in:
1082 T2_tilde.add(successor)
1083
1084 for predecessor in G2.pred[popped_node2]:
1085 if predecessor in reverse_mapping:
1086 # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1
1087 is_added = True
1088 T2.add(popped_node2)
1089 else:
1090 # check if its neighbor has another connection with a covered node.
1091 # If not, only then exclude it from T1
1092 if not any(pred in reverse_mapping for pred in G2.pred[predecessor]):
1093 T2.discard(predecessor)
1094
1095 if not any(succ in reverse_mapping for succ in G2[predecessor]):
1096 T2_in.discard(predecessor)
1097
1098 if not (predecessor in T2 or predecessor in T2_in):
1099 T2_tilde.add(predecessor)
1100
1101 if not is_added:
1102 T2_tilde.add(popped_node2)