aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99bin0 -> 1442 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99bin0 -> 1442 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99bin0 -> 310 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99bin0 -> 1602 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_ismags.py327
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphism.py48
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py410
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_match_helpers.py64
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py212
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py292
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp.py1608
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py3106
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py200
14 files changed, 6267 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99 b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99
new file mode 100644
index 00000000..dac54f00
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99 b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99
new file mode 100644
index 00000000..6c6af680
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99 b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99
new file mode 100644
index 00000000..60c3a3ce
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99 b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99
new file mode 100644
index 00000000..02368720
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_ismags.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_ismags.py
new file mode 100644
index 00000000..bc4070ac
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_ismags.py
@@ -0,0 +1,327 @@
+"""
+Tests for ISMAGS isomorphism algorithm.
+"""
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms import isomorphism as iso
+
+
+def _matches_to_sets(matches):
+ """
+ Helper function to facilitate comparing collections of dictionaries in
+ which order does not matter.
+ """
+ return {frozenset(m.items()) for m in matches}
+
+
+class TestSelfIsomorphism:
+ data = [
+ (
+ [
+ (0, {"name": "a"}),
+ (1, {"name": "a"}),
+ (2, {"name": "b"}),
+ (3, {"name": "b"}),
+ (4, {"name": "a"}),
+ (5, {"name": "a"}),
+ ],
+ [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)],
+ ),
+ (range(1, 5), [(1, 2), (2, 4), (4, 3), (3, 1)]),
+ (
+ [],
+ [
+ (0, 1),
+ (1, 2),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (5, 0),
+ (0, 6),
+ (6, 7),
+ (2, 8),
+ (8, 9),
+ (4, 10),
+ (10, 11),
+ ],
+ ),
+ ([], [(0, 1), (1, 2), (1, 4), (2, 3), (3, 5), (3, 6)]),
+ ]
+
+ def test_self_isomorphism(self):
+ """
+ For some small, symmetric graphs, make sure that 1) they are isomorphic
+ to themselves, and 2) that only the identity mapping is found.
+ """
+ for node_data, edge_data in self.data:
+ graph = nx.Graph()
+ graph.add_nodes_from(node_data)
+ graph.add_edges_from(edge_data)
+
+ ismags = iso.ISMAGS(
+ graph, graph, node_match=iso.categorical_node_match("name", None)
+ )
+ assert ismags.is_isomorphic()
+ assert ismags.subgraph_is_isomorphic()
+ assert list(ismags.subgraph_isomorphisms_iter(symmetry=True)) == [
+ {n: n for n in graph.nodes}
+ ]
+
+ def test_edgecase_self_isomorphism(self):
+ """
+ This edgecase is one of the cases in which it is hard to find all
+ symmetry elements.
+ """
+ graph = nx.Graph()
+ nx.add_path(graph, range(5))
+ graph.add_edges_from([(2, 5), (5, 6)])
+
+ ismags = iso.ISMAGS(graph, graph)
+ ismags_answer = list(ismags.find_isomorphisms(True))
+ assert ismags_answer == [{n: n for n in graph.nodes}]
+
+ graph = nx.relabel_nodes(graph, {0: 0, 1: 1, 2: 2, 3: 3, 4: 6, 5: 4, 6: 5})
+ ismags = iso.ISMAGS(graph, graph)
+ ismags_answer = list(ismags.find_isomorphisms(True))
+ assert ismags_answer == [{n: n for n in graph.nodes}]
+
+ def test_directed_self_isomorphism(self):
+ """
+ For some small, directed, symmetric graphs, make sure that 1) they are
+ isomorphic to themselves, and 2) that only the identity mapping is
+ found.
+ """
+ for node_data, edge_data in self.data:
+ graph = nx.Graph()
+ graph.add_nodes_from(node_data)
+ graph.add_edges_from(edge_data)
+
+ ismags = iso.ISMAGS(
+ graph, graph, node_match=iso.categorical_node_match("name", None)
+ )
+ assert ismags.is_isomorphic()
+ assert ismags.subgraph_is_isomorphic()
+ assert list(ismags.subgraph_isomorphisms_iter(symmetry=True)) == [
+ {n: n for n in graph.nodes}
+ ]
+
+
+class TestSubgraphIsomorphism:
+ def test_isomorphism(self):
+ g1 = nx.Graph()
+ nx.add_cycle(g1, range(4))
+
+ g2 = nx.Graph()
+ nx.add_cycle(g2, range(4))
+ g2.add_edges_from(list(zip(g2, range(4, 8))))
+ ismags = iso.ISMAGS(g2, g1)
+ assert list(ismags.subgraph_isomorphisms_iter(symmetry=True)) == [
+ {n: n for n in g1.nodes}
+ ]
+
+ def test_isomorphism2(self):
+ g1 = nx.Graph()
+ nx.add_path(g1, range(3))
+
+ g2 = g1.copy()
+ g2.add_edge(1, 3)
+
+ ismags = iso.ISMAGS(g2, g1)
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=True)
+ expected_symmetric = [
+ {0: 0, 1: 1, 2: 2},
+ {0: 0, 1: 1, 3: 2},
+ {2: 0, 1: 1, 3: 2},
+ ]
+ assert _matches_to_sets(matches) == _matches_to_sets(expected_symmetric)
+
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=False)
+ expected_asymmetric = [
+ {0: 2, 1: 1, 2: 0},
+ {0: 2, 1: 1, 3: 0},
+ {2: 2, 1: 1, 3: 0},
+ ]
+ assert _matches_to_sets(matches) == _matches_to_sets(
+ expected_symmetric + expected_asymmetric
+ )
+
+ def test_labeled_nodes(self):
+ g1 = nx.Graph()
+ nx.add_cycle(g1, range(3))
+ g1.nodes[1]["attr"] = True
+
+ g2 = g1.copy()
+ g2.add_edge(1, 3)
+ ismags = iso.ISMAGS(g2, g1, node_match=lambda x, y: x == y)
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=True)
+ expected_symmetric = [{0: 0, 1: 1, 2: 2}]
+ assert _matches_to_sets(matches) == _matches_to_sets(expected_symmetric)
+
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=False)
+ expected_asymmetric = [{0: 2, 1: 1, 2: 0}]
+ assert _matches_to_sets(matches) == _matches_to_sets(
+ expected_symmetric + expected_asymmetric
+ )
+
+ def test_labeled_edges(self):
+ g1 = nx.Graph()
+ nx.add_cycle(g1, range(3))
+ g1.edges[1, 2]["attr"] = True
+
+ g2 = g1.copy()
+ g2.add_edge(1, 3)
+ ismags = iso.ISMAGS(g2, g1, edge_match=lambda x, y: x == y)
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=True)
+ expected_symmetric = [{0: 0, 1: 1, 2: 2}]
+ assert _matches_to_sets(matches) == _matches_to_sets(expected_symmetric)
+
+ matches = ismags.subgraph_isomorphisms_iter(symmetry=False)
+ expected_asymmetric = [{1: 2, 0: 0, 2: 1}]
+ assert _matches_to_sets(matches) == _matches_to_sets(
+ expected_symmetric + expected_asymmetric
+ )
+
+
+class TestWikipediaExample:
+ # Nodes 'a', 'b', 'c' and 'd' form a column.
+ # Nodes 'g', 'h', 'i' and 'j' form a column.
+ g1edges = [
+ ["a", "g"],
+ ["a", "h"],
+ ["a", "i"],
+ ["b", "g"],
+ ["b", "h"],
+ ["b", "j"],
+ ["c", "g"],
+ ["c", "i"],
+ ["c", "j"],
+ ["d", "h"],
+ ["d", "i"],
+ ["d", "j"],
+ ]
+
+ # Nodes 1,2,3,4 form the clockwise corners of a large square.
+ # Nodes 5,6,7,8 form the clockwise corners of a small square
+ g2edges = [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 1],
+ [5, 6],
+ [6, 7],
+ [7, 8],
+ [8, 5],
+ [1, 5],
+ [2, 6],
+ [3, 7],
+ [4, 8],
+ ]
+
+ def test_graph(self):
+ g1 = nx.Graph()
+ g2 = nx.Graph()
+ g1.add_edges_from(self.g1edges)
+ g2.add_edges_from(self.g2edges)
+ gm = iso.ISMAGS(g1, g2)
+ assert gm.is_isomorphic()
+
+
+class TestLargestCommonSubgraph:
+ def test_mcis(self):
+ # Example graphs from DOI: 10.1002/spe.588
+ graph1 = nx.Graph()
+ graph1.add_edges_from([(1, 2), (2, 3), (2, 4), (3, 4), (4, 5)])
+ graph1.nodes[1]["color"] = 0
+
+ graph2 = nx.Graph()
+ graph2.add_edges_from(
+ [(1, 2), (2, 3), (2, 4), (3, 4), (3, 5), (5, 6), (5, 7), (6, 7)]
+ )
+ graph2.nodes[1]["color"] = 1
+ graph2.nodes[6]["color"] = 2
+ graph2.nodes[7]["color"] = 2
+
+ ismags = iso.ISMAGS(
+ graph1, graph2, node_match=iso.categorical_node_match("color", None)
+ )
+ assert list(ismags.subgraph_isomorphisms_iter(True)) == []
+ assert list(ismags.subgraph_isomorphisms_iter(False)) == []
+ found_mcis = _matches_to_sets(ismags.largest_common_subgraph())
+ expected = _matches_to_sets(
+ [{2: 2, 3: 4, 4: 3, 5: 5}, {2: 4, 3: 2, 4: 3, 5: 5}]
+ )
+ assert expected == found_mcis
+
+ ismags = iso.ISMAGS(
+ graph2, graph1, node_match=iso.categorical_node_match("color", None)
+ )
+ assert list(ismags.subgraph_isomorphisms_iter(True)) == []
+ assert list(ismags.subgraph_isomorphisms_iter(False)) == []
+ found_mcis = _matches_to_sets(ismags.largest_common_subgraph())
+ # Same answer, but reversed.
+ expected = _matches_to_sets(
+ [{2: 2, 3: 4, 4: 3, 5: 5}, {4: 2, 2: 3, 3: 4, 5: 5}]
+ )
+ assert expected == found_mcis
+
+ def test_symmetry_mcis(self):
+ graph1 = nx.Graph()
+ nx.add_path(graph1, range(4))
+
+ graph2 = nx.Graph()
+ nx.add_path(graph2, range(3))
+ graph2.add_edge(1, 3)
+
+ # Only the symmetry of graph2 is taken into account here.
+ ismags1 = iso.ISMAGS(
+ graph1, graph2, node_match=iso.categorical_node_match("color", None)
+ )
+ assert list(ismags1.subgraph_isomorphisms_iter(True)) == []
+ found_mcis = _matches_to_sets(ismags1.largest_common_subgraph())
+ expected = _matches_to_sets([{0: 0, 1: 1, 2: 2}, {1: 0, 3: 2, 2: 1}])
+ assert expected == found_mcis
+
+ # Only the symmetry of graph1 is taken into account here.
+ ismags2 = iso.ISMAGS(
+ graph2, graph1, node_match=iso.categorical_node_match("color", None)
+ )
+ assert list(ismags2.subgraph_isomorphisms_iter(True)) == []
+ found_mcis = _matches_to_sets(ismags2.largest_common_subgraph())
+ expected = _matches_to_sets(
+ [
+ {3: 2, 0: 0, 1: 1},
+ {2: 0, 0: 2, 1: 1},
+ {3: 0, 0: 2, 1: 1},
+ {3: 0, 1: 1, 2: 2},
+ {0: 0, 1: 1, 2: 2},
+ {2: 0, 3: 2, 1: 1},
+ ]
+ )
+
+ assert expected == found_mcis
+
+ found_mcis1 = _matches_to_sets(ismags1.largest_common_subgraph(False))
+ found_mcis2 = ismags2.largest_common_subgraph(False)
+ found_mcis2 = [{v: k for k, v in d.items()} for d in found_mcis2]
+ found_mcis2 = _matches_to_sets(found_mcis2)
+
+ expected = _matches_to_sets(
+ [
+ {3: 2, 1: 3, 2: 1},
+ {2: 0, 0: 2, 1: 1},
+ {1: 2, 3: 3, 2: 1},
+ {3: 0, 1: 3, 2: 1},
+ {0: 2, 2: 3, 1: 1},
+ {3: 0, 1: 2, 2: 1},
+ {2: 0, 0: 3, 1: 1},
+ {0: 0, 2: 3, 1: 1},
+ {1: 0, 3: 3, 2: 1},
+ {1: 0, 3: 2, 2: 1},
+ {0: 3, 1: 1, 2: 2},
+ {0: 0, 1: 1, 2: 2},
+ ]
+ )
+ assert expected == found_mcis1
+ assert expected == found_mcis2
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphism.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphism.py
new file mode 100644
index 00000000..548af808
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphism.py
@@ -0,0 +1,48 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms import isomorphism as iso
+
+
+class TestIsomorph:
+ @classmethod
+ def setup_class(cls):
+ cls.G1 = nx.Graph()
+ cls.G2 = nx.Graph()
+ cls.G3 = nx.Graph()
+ cls.G4 = nx.Graph()
+ cls.G5 = nx.Graph()
+ cls.G6 = nx.Graph()
+ cls.G1.add_edges_from([[1, 2], [1, 3], [1, 5], [2, 3]])
+ cls.G2.add_edges_from([[10, 20], [20, 30], [10, 30], [10, 50]])
+ cls.G3.add_edges_from([[1, 2], [1, 3], [1, 5], [2, 5]])
+ cls.G4.add_edges_from([[1, 2], [1, 3], [1, 5], [2, 4]])
+ cls.G5.add_edges_from([[1, 2], [1, 3]])
+ cls.G6.add_edges_from([[10, 20], [20, 30], [10, 30], [10, 50], [20, 50]])
+
+ def test_could_be_isomorphic(self):
+ assert iso.could_be_isomorphic(self.G1, self.G2)
+ assert iso.could_be_isomorphic(self.G1, self.G3)
+ assert not iso.could_be_isomorphic(self.G1, self.G4)
+ assert iso.could_be_isomorphic(self.G3, self.G2)
+ assert not iso.could_be_isomorphic(self.G1, self.G6)
+
+ def test_fast_could_be_isomorphic(self):
+ assert iso.fast_could_be_isomorphic(self.G3, self.G2)
+ assert not iso.fast_could_be_isomorphic(self.G3, self.G5)
+ assert not iso.fast_could_be_isomorphic(self.G1, self.G6)
+
+ def test_faster_could_be_isomorphic(self):
+ assert iso.faster_could_be_isomorphic(self.G3, self.G2)
+ assert not iso.faster_could_be_isomorphic(self.G3, self.G5)
+ assert not iso.faster_could_be_isomorphic(self.G1, self.G6)
+
+ def test_is_isomorphic(self):
+ assert iso.is_isomorphic(self.G1, self.G2)
+ assert not iso.is_isomorphic(self.G1, self.G4)
+ assert iso.is_isomorphic(self.G1.to_directed(), self.G2.to_directed())
+ assert not iso.is_isomorphic(self.G1.to_directed(), self.G4.to_directed())
+ with pytest.raises(
+ nx.NetworkXError, match="Graphs G1 and G2 are not of the same type."
+ ):
+ iso.is_isomorphic(self.G1.to_directed(), self.G1)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py
new file mode 100644
index 00000000..413dfaf3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py
@@ -0,0 +1,410 @@
+"""
+Tests for VF2 isomorphism algorithm.
+"""
+
+import importlib.resources
+import os
+import random
+import struct
+
+import networkx as nx
+from networkx.algorithms import isomorphism as iso
+
+
+class TestWikipediaExample:
+ # Source: https://en.wikipedia.org/wiki/Graph_isomorphism
+
+ # Nodes 'a', 'b', 'c' and 'd' form a column.
+ # Nodes 'g', 'h', 'i' and 'j' form a column.
+ g1edges = [
+ ["a", "g"],
+ ["a", "h"],
+ ["a", "i"],
+ ["b", "g"],
+ ["b", "h"],
+ ["b", "j"],
+ ["c", "g"],
+ ["c", "i"],
+ ["c", "j"],
+ ["d", "h"],
+ ["d", "i"],
+ ["d", "j"],
+ ]
+
+ # Nodes 1,2,3,4 form the clockwise corners of a large square.
+ # Nodes 5,6,7,8 form the clockwise corners of a small square
+ g2edges = [
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [4, 1],
+ [5, 6],
+ [6, 7],
+ [7, 8],
+ [8, 5],
+ [1, 5],
+ [2, 6],
+ [3, 7],
+ [4, 8],
+ ]
+
+ def test_graph(self):
+ g1 = nx.Graph()
+ g2 = nx.Graph()
+ g1.add_edges_from(self.g1edges)
+ g2.add_edges_from(self.g2edges)
+ gm = iso.GraphMatcher(g1, g2)
+ assert gm.is_isomorphic()
+ # Just testing some cases
+ assert gm.subgraph_is_monomorphic()
+
+ mapping = sorted(gm.mapping.items())
+
+ # this mapping is only one of the possibilities
+ # so this test needs to be reconsidered
+ # isomap = [('a', 1), ('b', 6), ('c', 3), ('d', 8),
+ # ('g', 2), ('h', 5), ('i', 4), ('j', 7)]
+ # assert_equal(mapping, isomap)
+
+ def test_subgraph(self):
+ g1 = nx.Graph()
+ g2 = nx.Graph()
+ g1.add_edges_from(self.g1edges)
+ g2.add_edges_from(self.g2edges)
+ g3 = g2.subgraph([1, 2, 3, 4])
+ gm = iso.GraphMatcher(g1, g3)
+ assert gm.subgraph_is_isomorphic()
+
+ def test_subgraph_mono(self):
+ g1 = nx.Graph()
+ g2 = nx.Graph()
+ g1.add_edges_from(self.g1edges)
+ g2.add_edges_from([[1, 2], [2, 3], [3, 4]])
+ gm = iso.GraphMatcher(g1, g2)
+ assert gm.subgraph_is_monomorphic()
+
+
+class TestVF2GraphDB:
+ # https://web.archive.org/web/20090303210205/http://amalfi.dis.unina.it/graph/db/
+
+ @staticmethod
+ def create_graph(filename):
+ """Creates a Graph instance from the filename."""
+
+ # The file is assumed to be in the format from the VF2 graph database.
+ # Each file is composed of 16-bit numbers (unsigned short int).
+ # So we will want to read 2 bytes at a time.
+
+ # We can read the number as follows:
+ # number = struct.unpack('<H', file.read(2))
+ # This says, expect the data in little-endian encoding
+ # as an unsigned short int and unpack 2 bytes from the file.
+
+ fh = open(filename, mode="rb")
+
+ # Grab the number of nodes.
+ # Node numeration is 0-based, so the first node has index 0.
+ nodes = struct.unpack("<H", fh.read(2))[0]
+
+ graph = nx.Graph()
+ for from_node in range(nodes):
+ # Get the number of edges.
+ edges = struct.unpack("<H", fh.read(2))[0]
+ for edge in range(edges):
+ # Get the terminal node.
+ to_node = struct.unpack("<H", fh.read(2))[0]
+ graph.add_edge(from_node, to_node)
+
+ fh.close()
+ return graph
+
+ def test_graph(self):
+ head = importlib.resources.files("networkx.algorithms.isomorphism.tests")
+ g1 = self.create_graph(head / "iso_r01_s80.A99")
+ g2 = self.create_graph(head / "iso_r01_s80.B99")
+ gm = iso.GraphMatcher(g1, g2)
+ assert gm.is_isomorphic()
+
+ def test_subgraph(self):
+ # A is the subgraph
+ # B is the full graph
+ head = importlib.resources.files("networkx.algorithms.isomorphism.tests")
+ subgraph = self.create_graph(head / "si2_b06_m200.A99")
+ graph = self.create_graph(head / "si2_b06_m200.B99")
+ gm = iso.GraphMatcher(graph, subgraph)
+ assert gm.subgraph_is_isomorphic()
+ # Just testing some cases
+ assert gm.subgraph_is_monomorphic()
+
+ # There isn't a similar test implemented for subgraph monomorphism,
+ # feel free to create one.
+
+
+class TestAtlas:
+ @classmethod
+ def setup_class(cls):
+ global atlas
+ from networkx.generators import atlas
+
+ cls.GAG = atlas.graph_atlas_g()
+
+ def test_graph_atlas(self):
+ # Atlas = nx.graph_atlas_g()[0:208] # 208, 6 nodes or less
+ Atlas = self.GAG[0:100]
+ alphabet = list(range(26))
+ for graph in Atlas:
+ nlist = list(graph)
+ labels = alphabet[: len(nlist)]
+ for s in range(10):
+ random.shuffle(labels)
+ d = dict(zip(nlist, labels))
+ relabel = nx.relabel_nodes(graph, d)
+ gm = iso.GraphMatcher(graph, relabel)
+ assert gm.is_isomorphic()
+
+
+def test_multiedge():
+ # Simple test for multigraphs
+ # Need something much more rigorous
+ edges = [
+ (0, 1),
+ (1, 2),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (5, 6),
+ (6, 7),
+ (7, 8),
+ (8, 9),
+ (9, 10),
+ (10, 11),
+ (10, 11),
+ (11, 12),
+ (11, 12),
+ (12, 13),
+ (12, 13),
+ (13, 14),
+ (13, 14),
+ (14, 15),
+ (14, 15),
+ (15, 16),
+ (15, 16),
+ (16, 17),
+ (16, 17),
+ (17, 18),
+ (17, 18),
+ (18, 19),
+ (18, 19),
+ (19, 0),
+ (19, 0),
+ ]
+ nodes = list(range(20))
+
+ for g1 in [nx.MultiGraph(), nx.MultiDiGraph()]:
+ g1.add_edges_from(edges)
+ for _ in range(10):
+ new_nodes = list(nodes)
+ random.shuffle(new_nodes)
+ d = dict(zip(nodes, new_nodes))
+ g2 = nx.relabel_nodes(g1, d)
+ if not g1.is_directed():
+ gm = iso.GraphMatcher(g1, g2)
+ else:
+ gm = iso.DiGraphMatcher(g1, g2)
+ assert gm.is_isomorphic()
+ # Testing if monomorphism works in multigraphs
+ assert gm.subgraph_is_monomorphic()
+
+
+def test_selfloop():
+ # Simple test for graphs with selfloops
+ edges = [
+ (0, 1),
+ (0, 2),
+ (1, 2),
+ (1, 3),
+ (2, 2),
+ (2, 4),
+ (3, 1),
+ (3, 2),
+ (4, 2),
+ (4, 5),
+ (5, 4),
+ ]
+ nodes = list(range(6))
+
+ for g1 in [nx.Graph(), nx.DiGraph()]:
+ g1.add_edges_from(edges)
+ for _ in range(100):
+ new_nodes = list(nodes)
+ random.shuffle(new_nodes)
+ d = dict(zip(nodes, new_nodes))
+ g2 = nx.relabel_nodes(g1, d)
+ if not g1.is_directed():
+ gm = iso.GraphMatcher(g1, g2)
+ else:
+ gm = iso.DiGraphMatcher(g1, g2)
+ assert gm.is_isomorphic()
+
+
+def test_selfloop_mono():
+ # Simple test for graphs with selfloops
+ edges0 = [
+ (0, 1),
+ (0, 2),
+ (1, 2),
+ (1, 3),
+ (2, 4),
+ (3, 1),
+ (3, 2),
+ (4, 2),
+ (4, 5),
+ (5, 4),
+ ]
+ edges = edges0 + [(2, 2)]
+ nodes = list(range(6))
+
+ for g1 in [nx.Graph(), nx.DiGraph()]:
+ g1.add_edges_from(edges)
+ for _ in range(100):
+ new_nodes = list(nodes)
+ random.shuffle(new_nodes)
+ d = dict(zip(nodes, new_nodes))
+ g2 = nx.relabel_nodes(g1, d)
+ g2.remove_edges_from(nx.selfloop_edges(g2))
+ if not g1.is_directed():
+ gm = iso.GraphMatcher(g2, g1)
+ else:
+ gm = iso.DiGraphMatcher(g2, g1)
+ assert not gm.subgraph_is_monomorphic()
+
+
+def test_isomorphism_iter1():
+ # As described in:
+ # http://groups.google.com/group/networkx-discuss/browse_thread/thread/2ff65c67f5e3b99f/d674544ebea359bb?fwc=1
+ g1 = nx.DiGraph()
+ g2 = nx.DiGraph()
+ g3 = nx.DiGraph()
+ g1.add_edge("A", "B")
+ g1.add_edge("B", "C")
+ g2.add_edge("Y", "Z")
+ g3.add_edge("Z", "Y")
+ gm12 = iso.DiGraphMatcher(g1, g2)
+ gm13 = iso.DiGraphMatcher(g1, g3)
+ x = list(gm12.subgraph_isomorphisms_iter())
+ y = list(gm13.subgraph_isomorphisms_iter())
+ assert {"A": "Y", "B": "Z"} in x
+ assert {"B": "Y", "C": "Z"} in x
+ assert {"A": "Z", "B": "Y"} in y
+ assert {"B": "Z", "C": "Y"} in y
+ assert len(x) == len(y)
+ assert len(x) == 2
+
+
+def test_monomorphism_iter1():
+ g1 = nx.DiGraph()
+ g2 = nx.DiGraph()
+ g1.add_edge("A", "B")
+ g1.add_edge("B", "C")
+ g1.add_edge("C", "A")
+ g2.add_edge("X", "Y")
+ g2.add_edge("Y", "Z")
+ gm12 = iso.DiGraphMatcher(g1, g2)
+ x = list(gm12.subgraph_monomorphisms_iter())
+ assert {"A": "X", "B": "Y", "C": "Z"} in x
+ assert {"A": "Y", "B": "Z", "C": "X"} in x
+ assert {"A": "Z", "B": "X", "C": "Y"} in x
+ assert len(x) == 3
+ gm21 = iso.DiGraphMatcher(g2, g1)
+ # Check if StopIteration exception returns False
+ assert not gm21.subgraph_is_monomorphic()
+
+
+def test_isomorphism_iter2():
+ # Path
+ for L in range(2, 10):
+ g1 = nx.path_graph(L)
+ gm = iso.GraphMatcher(g1, g1)
+ s = len(list(gm.isomorphisms_iter()))
+ assert s == 2
+ # Cycle
+ for L in range(3, 10):
+ g1 = nx.cycle_graph(L)
+ gm = iso.GraphMatcher(g1, g1)
+ s = len(list(gm.isomorphisms_iter()))
+ assert s == 2 * L
+
+
+def test_multiple():
+ # Verify that we can use the graph matcher multiple times
+ edges = [("A", "B"), ("B", "A"), ("B", "C")]
+ for g1, g2 in [(nx.Graph(), nx.Graph()), (nx.DiGraph(), nx.DiGraph())]:
+ g1.add_edges_from(edges)
+ g2.add_edges_from(edges)
+ g3 = nx.subgraph(g2, ["A", "B"])
+ if not g1.is_directed():
+ gmA = iso.GraphMatcher(g1, g2)
+ gmB = iso.GraphMatcher(g1, g3)
+ else:
+ gmA = iso.DiGraphMatcher(g1, g2)
+ gmB = iso.DiGraphMatcher(g1, g3)
+ assert gmA.is_isomorphic()
+ g2.remove_node("C")
+ if not g1.is_directed():
+ gmA = iso.GraphMatcher(g1, g2)
+ else:
+ gmA = iso.DiGraphMatcher(g1, g2)
+ assert gmA.subgraph_is_isomorphic()
+ assert gmB.subgraph_is_isomorphic()
+ assert gmA.subgraph_is_monomorphic()
+ assert gmB.subgraph_is_monomorphic()
+
+
+# for m in [gmB.mapping, gmB.mapping]:
+# assert_true(m['A'] == 'A')
+# assert_true(m['B'] == 'B')
+# assert_true('C' not in m)
+
+
+def test_noncomparable_nodes():
+ node1 = object()
+ node2 = object()
+ node3 = object()
+
+ # Graph
+ G = nx.path_graph([node1, node2, node3])
+ gm = iso.GraphMatcher(G, G)
+ assert gm.is_isomorphic()
+ # Just testing some cases
+ assert gm.subgraph_is_monomorphic()
+
+ # DiGraph
+ G = nx.path_graph([node1, node2, node3], create_using=nx.DiGraph)
+ H = nx.path_graph([node3, node2, node1], create_using=nx.DiGraph)
+ dgm = iso.DiGraphMatcher(G, H)
+ assert dgm.is_isomorphic()
+ # Just testing some cases
+ assert gm.subgraph_is_monomorphic()
+
+
+def test_monomorphism_edge_match():
+ G = nx.DiGraph()
+ G.add_node(1)
+ G.add_node(2)
+ G.add_edge(1, 2, label="A")
+ G.add_edge(2, 1, label="B")
+ G.add_edge(2, 2, label="C")
+
+ SG = nx.DiGraph()
+ SG.add_node(5)
+ SG.add_node(6)
+ SG.add_edge(5, 6, label="A")
+
+ gm = iso.DiGraphMatcher(G, SG, edge_match=iso.categorical_edge_match("label", None))
+ assert gm.subgraph_is_monomorphic()
+
+
+def test_isomorphvf2pp_multidigraphs():
+ g = nx.MultiDiGraph({0: [1, 1, 2, 2, 3], 1: [2, 3, 3], 2: [3]})
+ h = nx.MultiDiGraph({0: [1, 1, 2, 2, 3], 1: [2, 3, 3], 3: [2]})
+ assert not (nx.vf2pp_is_isomorphic(g, h))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_match_helpers.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_match_helpers.py
new file mode 100644
index 00000000..4d70347f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_match_helpers.py
@@ -0,0 +1,64 @@
+from operator import eq
+
+import networkx as nx
+from networkx.algorithms import isomorphism as iso
+
+
+def test_categorical_node_match():
+ nm = iso.categorical_node_match(["x", "y", "z"], [None] * 3)
+ assert nm({"x": 1, "y": 2, "z": 3}, {"x": 1, "y": 2, "z": 3})
+ assert not nm({"x": 1, "y": 2, "z": 2}, {"x": 1, "y": 2, "z": 1})
+
+
+class TestGenericMultiEdgeMatch:
+ def setup_method(self):
+ self.G1 = nx.MultiDiGraph()
+ self.G2 = nx.MultiDiGraph()
+ self.G3 = nx.MultiDiGraph()
+ self.G4 = nx.MultiDiGraph()
+ attr_dict1 = {"id": "edge1", "minFlow": 0, "maxFlow": 10}
+ attr_dict2 = {"id": "edge2", "minFlow": -3, "maxFlow": 7}
+ attr_dict3 = {"id": "edge3", "minFlow": 13, "maxFlow": 117}
+ attr_dict4 = {"id": "edge4", "minFlow": 13, "maxFlow": 117}
+ attr_dict5 = {"id": "edge5", "minFlow": 8, "maxFlow": 12}
+ attr_dict6 = {"id": "edge6", "minFlow": 8, "maxFlow": 12}
+ for attr_dict in [
+ attr_dict1,
+ attr_dict2,
+ attr_dict3,
+ attr_dict4,
+ attr_dict5,
+ attr_dict6,
+ ]:
+ self.G1.add_edge(1, 2, **attr_dict)
+ for attr_dict in [
+ attr_dict5,
+ attr_dict3,
+ attr_dict6,
+ attr_dict1,
+ attr_dict4,
+ attr_dict2,
+ ]:
+ self.G2.add_edge(2, 3, **attr_dict)
+ for attr_dict in [attr_dict3, attr_dict5]:
+ self.G3.add_edge(3, 4, **attr_dict)
+ for attr_dict in [attr_dict6, attr_dict4]:
+ self.G4.add_edge(4, 5, **attr_dict)
+
+ def test_generic_multiedge_match(self):
+ full_match = iso.generic_multiedge_match(
+ ["id", "flowMin", "flowMax"], [None] * 3, [eq] * 3
+ )
+ flow_match = iso.generic_multiedge_match(
+ ["flowMin", "flowMax"], [None] * 2, [eq] * 2
+ )
+ min_flow_match = iso.generic_multiedge_match("flowMin", None, eq)
+ id_match = iso.generic_multiedge_match("id", None, eq)
+ assert flow_match(self.G1[1][2], self.G2[2][3])
+ assert min_flow_match(self.G1[1][2], self.G2[2][3])
+ assert id_match(self.G1[1][2], self.G2[2][3])
+ assert full_match(self.G1[1][2], self.G2[2][3])
+ assert flow_match(self.G3[3][4], self.G4[4][5])
+ assert min_flow_match(self.G3[3][4], self.G4[4][5])
+ assert not id_match(self.G3[3][4], self.G4[4][5])
+ assert not full_match(self.G3[3][4], self.G4[4][5])
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py
new file mode 100644
index 00000000..1fe70a42
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py
@@ -0,0 +1,212 @@
+"""
+Tests for the temporal aspect of the Temporal VF2 isomorphism algorithm.
+"""
+
+from datetime import date, datetime, timedelta
+
+import networkx as nx
+from networkx.algorithms import isomorphism as iso
+
+
+def provide_g1_edgelist():
+ return [(0, 1), (0, 2), (1, 2), (2, 4), (1, 3), (3, 4), (4, 5)]
+
+
+def put_same_time(G, att_name):
+ for e in G.edges(data=True):
+ e[2][att_name] = date(2015, 1, 1)
+ return G
+
+
+def put_same_datetime(G, att_name):
+ for e in G.edges(data=True):
+ e[2][att_name] = datetime(2015, 1, 1)
+ return G
+
+
+def put_sequence_time(G, att_name):
+ current_date = date(2015, 1, 1)
+ for e in G.edges(data=True):
+ current_date += timedelta(days=1)
+ e[2][att_name] = current_date
+ return G
+
+
+def put_time_config_0(G, att_name):
+ G[0][1][att_name] = date(2015, 1, 2)
+ G[0][2][att_name] = date(2015, 1, 2)
+ G[1][2][att_name] = date(2015, 1, 3)
+ G[1][3][att_name] = date(2015, 1, 1)
+ G[2][4][att_name] = date(2015, 1, 1)
+ G[3][4][att_name] = date(2015, 1, 3)
+ G[4][5][att_name] = date(2015, 1, 3)
+ return G
+
+
+def put_time_config_1(G, att_name):
+ G[0][1][att_name] = date(2015, 1, 2)
+ G[0][2][att_name] = date(2015, 1, 1)
+ G[1][2][att_name] = date(2015, 1, 3)
+ G[1][3][att_name] = date(2015, 1, 1)
+ G[2][4][att_name] = date(2015, 1, 2)
+ G[3][4][att_name] = date(2015, 1, 4)
+ G[4][5][att_name] = date(2015, 1, 3)
+ return G
+
+
+def put_time_config_2(G, att_name):
+ G[0][1][att_name] = date(2015, 1, 1)
+ G[0][2][att_name] = date(2015, 1, 1)
+ G[1][2][att_name] = date(2015, 1, 3)
+ G[1][3][att_name] = date(2015, 1, 2)
+ G[2][4][att_name] = date(2015, 1, 2)
+ G[3][4][att_name] = date(2015, 1, 3)
+ G[4][5][att_name] = date(2015, 1, 2)
+ return G
+
+
+class TestTimeRespectingGraphMatcher:
+ """
+ A test class for the undirected temporal graph matcher.
+ """
+
+ def provide_g1_topology(self):
+ G1 = nx.Graph()
+ G1.add_edges_from(provide_g1_edgelist())
+ return G1
+
+ def provide_g2_path_3edges(self):
+ G2 = nx.Graph()
+ G2.add_edges_from([(0, 1), (1, 2), (2, 3)])
+ return G2
+
+ def test_timdelta_zero_timeRespecting_returnsTrue(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_same_time(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta()
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
+ assert gm.subgraph_is_isomorphic()
+
+ def test_timdelta_zero_datetime_timeRespecting_returnsTrue(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_same_datetime(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta()
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
+ assert gm.subgraph_is_isomorphic()
+
+ def test_attNameStrange_timdelta_zero_timeRespecting_returnsTrue(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "strange_name"
+ G1 = put_same_time(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta()
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
+ assert gm.subgraph_is_isomorphic()
+
+ def test_notTimeRespecting_returnsFalse(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_sequence_time(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta()
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
+ assert not gm.subgraph_is_isomorphic()
+
+ def test_timdelta_one_config0_returns_no_embeddings(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_time_config_0(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta(days=1)
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
+ assert count_match == 0
+
+ def test_timdelta_one_config1_returns_four_embedding(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_time_config_1(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta(days=1)
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
+ assert count_match == 4
+
+ def test_timdelta_one_config2_returns_ten_embeddings(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_time_config_2(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta(days=1)
+ gm = iso.TimeRespectingGraphMatcher(G1, G2, temporal_name, d)
+ L = list(gm.subgraph_isomorphisms_iter())
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
+ assert count_match == 10
+
+
+class TestDiTimeRespectingGraphMatcher:
+ """
+ A test class for the directed time-respecting graph matcher.
+ """
+
+ def provide_g1_topology(self):
+ G1 = nx.DiGraph()
+ G1.add_edges_from(provide_g1_edgelist())
+ return G1
+
+ def provide_g2_path_3edges(self):
+ G2 = nx.DiGraph()
+ G2.add_edges_from([(0, 1), (1, 2), (2, 3)])
+ return G2
+
+ def test_timdelta_zero_same_dates_returns_true(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_same_time(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta()
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
+ assert gm.subgraph_is_isomorphic()
+
+ def test_attNameStrange_timdelta_zero_same_dates_returns_true(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "strange"
+ G1 = put_same_time(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta()
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
+ assert gm.subgraph_is_isomorphic()
+
+ def test_timdelta_one_config0_returns_no_embeddings(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_time_config_0(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta(days=1)
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
+ assert count_match == 0
+
+ def test_timdelta_one_config1_returns_one_embedding(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_time_config_1(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta(days=1)
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
+ assert count_match == 1
+
+ def test_timdelta_one_config2_returns_two_embeddings(self):
+ G1 = self.provide_g1_topology()
+ temporal_name = "date"
+ G1 = put_time_config_2(G1, temporal_name)
+ G2 = self.provide_g2_path_3edges()
+ d = timedelta(days=1)
+ gm = iso.TimeRespectingDiGraphMatcher(G1, G2, temporal_name, d)
+ count_match = len(list(gm.subgraph_isomorphisms_iter()))
+ assert count_match == 2
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py
new file mode 100644
index 00000000..fa1ab9bb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py
@@ -0,0 +1,292 @@
+import random
+import time
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.isomorphism.tree_isomorphism import (
+ rooted_tree_isomorphism,
+ tree_isomorphism,
+)
+from networkx.classes.function import is_directed
+
+
+@pytest.mark.parametrize("graph_constructor", (nx.DiGraph, nx.MultiGraph))
+def test_tree_isomorphism_raises_on_directed_and_multigraphs(graph_constructor):
+ t1 = graph_constructor([(0, 1)])
+ t2 = graph_constructor([(1, 2)])
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.isomorphism.tree_isomorphism(t1, t2)
+
+
+# have this work for graph
+# given two trees (either the directed or undirected)
+# transform t2 according to the isomorphism
+# and confirm it is identical to t1
+# randomize the order of the edges when constructing
+def check_isomorphism(t1, t2, isomorphism):
+ # get the name of t1, given the name in t2
+ mapping = {v2: v1 for (v1, v2) in isomorphism}
+
+ # these should be the same
+ d1 = is_directed(t1)
+ d2 = is_directed(t2)
+ assert d1 == d2
+
+ edges_1 = []
+ for u, v in t1.edges():
+ if d1:
+ edges_1.append((u, v))
+ else:
+ # if not directed, then need to
+ # put the edge in a consistent direction
+ if u < v:
+ edges_1.append((u, v))
+ else:
+ edges_1.append((v, u))
+
+ edges_2 = []
+ for u, v in t2.edges():
+ # translate to names for t1
+ u = mapping[u]
+ v = mapping[v]
+ if d2:
+ edges_2.append((u, v))
+ else:
+ if u < v:
+ edges_2.append((u, v))
+ else:
+ edges_2.append((v, u))
+
+ return sorted(edges_1) == sorted(edges_2)
+
+
+def test_hardcoded():
+ print("hardcoded test")
+
+ # define a test problem
+ edges_1 = [
+ ("a", "b"),
+ ("a", "c"),
+ ("a", "d"),
+ ("b", "e"),
+ ("b", "f"),
+ ("e", "j"),
+ ("e", "k"),
+ ("c", "g"),
+ ("c", "h"),
+ ("g", "m"),
+ ("d", "i"),
+ ("f", "l"),
+ ]
+
+ edges_2 = [
+ ("v", "y"),
+ ("v", "z"),
+ ("u", "x"),
+ ("q", "u"),
+ ("q", "v"),
+ ("p", "t"),
+ ("n", "p"),
+ ("n", "q"),
+ ("n", "o"),
+ ("o", "r"),
+ ("o", "s"),
+ ("s", "w"),
+ ]
+
+ # there are two possible correct isomorphisms
+ # it currently returns isomorphism1
+ # but the second is also correct
+ isomorphism1 = [
+ ("a", "n"),
+ ("b", "q"),
+ ("c", "o"),
+ ("d", "p"),
+ ("e", "v"),
+ ("f", "u"),
+ ("g", "s"),
+ ("h", "r"),
+ ("i", "t"),
+ ("j", "y"),
+ ("k", "z"),
+ ("l", "x"),
+ ("m", "w"),
+ ]
+
+ # could swap y and z
+ isomorphism2 = [
+ ("a", "n"),
+ ("b", "q"),
+ ("c", "o"),
+ ("d", "p"),
+ ("e", "v"),
+ ("f", "u"),
+ ("g", "s"),
+ ("h", "r"),
+ ("i", "t"),
+ ("j", "z"),
+ ("k", "y"),
+ ("l", "x"),
+ ("m", "w"),
+ ]
+
+ t1 = nx.Graph()
+ t1.add_edges_from(edges_1)
+ root1 = "a"
+
+ t2 = nx.Graph()
+ t2.add_edges_from(edges_2)
+ root2 = "n"
+
+ isomorphism = sorted(rooted_tree_isomorphism(t1, root1, t2, root2))
+
+ # is correct by hand
+ assert isomorphism in (isomorphism1, isomorphism2)
+
+ # check algorithmically
+ assert check_isomorphism(t1, t2, isomorphism)
+
+ # try again as digraph
+ t1 = nx.DiGraph()
+ t1.add_edges_from(edges_1)
+ root1 = "a"
+
+ t2 = nx.DiGraph()
+ t2.add_edges_from(edges_2)
+ root2 = "n"
+
+ isomorphism = sorted(rooted_tree_isomorphism(t1, root1, t2, root2))
+
+ # is correct by hand
+ assert isomorphism in (isomorphism1, isomorphism2)
+
+ # check algorithmically
+ assert check_isomorphism(t1, t2, isomorphism)
+
+
+# randomly swap a tuple (a,b)
+def random_swap(t):
+ (a, b) = t
+ if random.randint(0, 1) == 1:
+ return (a, b)
+ else:
+ return (b, a)
+
+
+# given a tree t1, create a new tree t2
+# that is isomorphic to t1, with a known isomorphism
+# and test that our algorithm found the right one
+def positive_single_tree(t1):
+ assert nx.is_tree(t1)
+
+ nodes1 = list(t1.nodes())
+ # get a random permutation of this
+ nodes2 = nodes1.copy()
+ random.shuffle(nodes2)
+
+ # this is one isomorphism, however they may be multiple
+ # so we don't necessarily get this one back
+ someisomorphism = list(zip(nodes1, nodes2))
+
+ # map from old to new
+ map1to2 = dict(someisomorphism)
+
+ # get the edges with the transformed names
+ edges2 = [random_swap((map1to2[u], map1to2[v])) for (u, v) in t1.edges()]
+ # randomly permute, to ensure we're not relying on edge order somehow
+ random.shuffle(edges2)
+
+ # so t2 is isomorphic to t1
+ t2 = nx.Graph()
+ t2.add_edges_from(edges2)
+
+ # lets call our code to see if t1 and t2 are isomorphic
+ isomorphism = tree_isomorphism(t1, t2)
+
+ # make sure we got a correct solution
+ # although not necessarily someisomorphism
+ assert len(isomorphism) > 0
+ assert check_isomorphism(t1, t2, isomorphism)
+
+
+# run positive_single_tree over all the
+# non-isomorphic trees for k from 4 to maxk
+# k = 4 is the first level that has more than 1 non-isomorphic tree
+# k = 13 takes about 2.86 seconds to run on my laptop
+# larger values run slow down significantly
+# as the number of trees grows rapidly
+def test_positive(maxk=14):
+ print("positive test")
+
+ for k in range(2, maxk + 1):
+ start_time = time.time()
+ trial = 0
+ for t in nx.nonisomorphic_trees(k):
+ positive_single_tree(t)
+ trial += 1
+ print(k, trial, time.time() - start_time)
+
+
+# test the trivial case of a single node in each tree
+# note that nonisomorphic_trees doesn't work for k = 1
+def test_trivial():
+ print("trivial test")
+
+ # back to an undirected graph
+ t1 = nx.Graph()
+ t1.add_node("a")
+ root1 = "a"
+
+ t2 = nx.Graph()
+ t2.add_node("n")
+ root2 = "n"
+
+ isomorphism = rooted_tree_isomorphism(t1, root1, t2, root2)
+
+ assert isomorphism == [("a", "n")]
+
+ assert check_isomorphism(t1, t2, isomorphism)
+
+
+# test another trivial case where the two graphs have
+# different numbers of nodes
+def test_trivial_2():
+ print("trivial test 2")
+
+ edges_1 = [("a", "b"), ("a", "c")]
+
+ edges_2 = [("v", "y")]
+
+ t1 = nx.Graph()
+ t1.add_edges_from(edges_1)
+
+ t2 = nx.Graph()
+ t2.add_edges_from(edges_2)
+
+ isomorphism = tree_isomorphism(t1, t2)
+
+ # they cannot be isomorphic,
+ # since they have different numbers of nodes
+ assert isomorphism == []
+
+
+# the function nonisomorphic_trees generates all the non-isomorphic
+# trees of a given size. Take each pair of these and verify that
+# they are not isomorphic
+# k = 4 is the first level that has more than 1 non-isomorphic tree
+# k = 11 takes about 4.76 seconds to run on my laptop
+# larger values run slow down significantly
+# as the number of trees grows rapidly
+def test_negative(maxk=11):
+ print("negative test")
+
+ for k in range(4, maxk + 1):
+ test_trees = list(nx.nonisomorphic_trees(k))
+ start_time = time.time()
+ trial = 0
+ for i in range(len(test_trees) - 1):
+ for j in range(i + 1, len(test_trees)):
+ trial += 1
+ assert tree_isomorphism(test_trees[i], test_trees[j]) == []
+ print(k, trial, time.time() - start_time)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp.py
new file mode 100644
index 00000000..5f3fb901
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp.py
@@ -0,0 +1,1608 @@
+import itertools as it
+
+import pytest
+
+import networkx as nx
+from networkx import vf2pp_is_isomorphic, vf2pp_isomorphism
+
+labels_same = ["blue"]
+
+labels_many = [
+ "white",
+ "red",
+ "blue",
+ "green",
+ "orange",
+ "black",
+ "purple",
+ "yellow",
+ "brown",
+ "cyan",
+ "solarized",
+ "pink",
+ "none",
+]
+
+
+class TestPreCheck:
+ def test_first_graph_empty(self):
+ G1 = nx.Graph()
+ G2 = nx.Graph([(0, 1), (1, 2)])
+ assert not vf2pp_is_isomorphic(G1, G2)
+
+ def test_second_graph_empty(self):
+ G1 = nx.Graph([(0, 1), (1, 2)])
+ G2 = nx.Graph()
+ assert not vf2pp_is_isomorphic(G1, G2)
+
+ def test_different_order1(self):
+ G1 = nx.path_graph(5)
+ G2 = nx.path_graph(6)
+ assert not vf2pp_is_isomorphic(G1, G2)
+
+ def test_different_order2(self):
+ G1 = nx.barbell_graph(100, 20)
+ G2 = nx.barbell_graph(101, 20)
+ assert not vf2pp_is_isomorphic(G1, G2)
+
+ def test_different_order3(self):
+ G1 = nx.complete_graph(7)
+ G2 = nx.complete_graph(8)
+ assert not vf2pp_is_isomorphic(G1, G2)
+
+ def test_different_degree_sequences1(self):
+ G1 = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (0, 4)])
+ G2 = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (0, 4), (2, 5)])
+ assert not vf2pp_is_isomorphic(G1, G2)
+
+ G2.remove_node(3)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(["a"]))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle("a"))), "label")
+
+ assert vf2pp_is_isomorphic(G1, G2)
+
+ def test_different_degree_sequences2(self):
+ G1 = nx.Graph(
+ [
+ (0, 1),
+ (1, 2),
+ (0, 2),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (5, 6),
+ (6, 3),
+ (4, 7),
+ (7, 8),
+ (8, 3),
+ ]
+ )
+ G2 = G1.copy()
+ G2.add_edge(8, 0)
+ assert not vf2pp_is_isomorphic(G1, G2)
+
+ G1.add_edge(6, 1)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(["a"]))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle("a"))), "label")
+
+ assert vf2pp_is_isomorphic(G1, G2)
+
+ def test_different_degree_sequences3(self):
+ G1 = nx.Graph([(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (2, 5), (2, 6)])
+ G2 = nx.Graph(
+ [(0, 1), (0, 6), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (2, 5), (2, 6)]
+ )
+ assert not vf2pp_is_isomorphic(G1, G2)
+
+ G1.add_edge(3, 5)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(["a"]))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle("a"))), "label")
+
+ assert vf2pp_is_isomorphic(G1, G2)
+
+ def test_label_distribution(self):
+ G1 = nx.Graph([(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (2, 5), (2, 6)])
+ G2 = nx.Graph([(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (2, 5), (2, 6)])
+
+ colors1 = ["blue", "blue", "blue", "yellow", "black", "purple", "purple"]
+ colors2 = ["blue", "blue", "yellow", "yellow", "black", "purple", "purple"]
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(colors1[::-1]))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(colors2[::-1]))), "label")
+
+ assert not vf2pp_is_isomorphic(G1, G2, node_label="label")
+ G2.nodes[3]["label"] = "blue"
+ assert vf2pp_is_isomorphic(G1, G2, node_label="label")
+
+
+class TestAllGraphTypesEdgeCases:
+ @pytest.mark.parametrize("graph_type", (nx.Graph, nx.MultiGraph, nx.DiGraph))
+ def test_both_graphs_empty(self, graph_type):
+ G = graph_type()
+ H = graph_type()
+ assert vf2pp_isomorphism(G, H) is None
+
+ G.add_node(0)
+
+ assert vf2pp_isomorphism(G, H) is None
+ assert vf2pp_isomorphism(H, G) is None
+
+ H.add_node(0)
+ assert vf2pp_isomorphism(G, H) == {0: 0}
+
+ @pytest.mark.parametrize("graph_type", (nx.Graph, nx.MultiGraph, nx.DiGraph))
+ def test_first_graph_empty(self, graph_type):
+ G = graph_type()
+ H = graph_type([(0, 1)])
+ assert vf2pp_isomorphism(G, H) is None
+
+ @pytest.mark.parametrize("graph_type", (nx.Graph, nx.MultiGraph, nx.DiGraph))
+ def test_second_graph_empty(self, graph_type):
+ G = graph_type([(0, 1)])
+ H = graph_type()
+ assert vf2pp_isomorphism(G, H) is None
+
+
+class TestGraphISOVF2pp:
+ def test_custom_graph1_same_labels(self):
+ G1 = nx.Graph()
+
+ mapped = {1: "A", 2: "B", 3: "C", 4: "D", 5: "Z", 6: "E"}
+ edges1 = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 6), (3, 4), (5, 1), (5, 2)]
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Add edge making G1 symmetrical
+ G1.add_edge(3, 7)
+ G1.nodes[7]["label"] = "blue"
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
+
+ # Make G2 isomorphic to G1
+ G2.add_edges_from([(mapped[3], "X"), (mapped[6], mapped[5])])
+ G1.add_edge(4, 7)
+ G2.nodes["X"]["label"] = "blue"
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Re-structure maintaining isomorphism
+ G1.remove_edges_from([(1, 4), (1, 3)])
+ G2.remove_edges_from([(mapped[1], mapped[5]), (mapped[1], mapped[2])])
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ def test_custom_graph1_different_labels(self):
+ G1 = nx.Graph()
+
+ mapped = {1: "A", 2: "B", 3: "C", 4: "D", 5: "Z", 6: "E"}
+ edges1 = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 6), (3, 4), (5, 1), (5, 2)]
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
+
+ def test_custom_graph2_same_labels(self):
+ G1 = nx.Graph()
+
+ mapped = {1: "A", 2: "C", 3: "D", 4: "E", 5: "G", 7: "B", 6: "F"}
+ edges1 = [(1, 2), (1, 5), (5, 6), (2, 3), (2, 4), (3, 4), (4, 5), (2, 7)]
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Obtain two isomorphic subgraphs from the graph
+ G2.remove_edge(mapped[1], mapped[2])
+ G2.add_edge(mapped[1], mapped[4])
+ H1 = nx.Graph(G1.subgraph([2, 3, 4, 7]))
+ H2 = nx.Graph(G2.subgraph([mapped[1], mapped[4], mapped[5], mapped[6]]))
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
+
+ # Add edges maintaining isomorphism
+ H1.add_edges_from([(3, 7), (4, 7)])
+ H2.add_edges_from([(mapped[1], mapped[6]), (mapped[4], mapped[6])])
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
+
+ def test_custom_graph2_different_labels(self):
+ G1 = nx.Graph()
+
+ mapped = {1: "A", 2: "C", 3: "D", 4: "E", 5: "G", 7: "B", 6: "F"}
+ edges1 = [(1, 2), (1, 5), (5, 6), (2, 3), (2, 4), (3, 4), (4, 5), (2, 7)]
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+
+ # Adding new nodes
+ G1.add_node(0)
+ G2.add_node("Z")
+ G1.nodes[0]["label"] = G1.nodes[1]["label"]
+ G2.nodes["Z"]["label"] = G1.nodes[1]["label"]
+ mapped.update({0: "Z"})
+
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
+
+ # Change the color of one of the nodes
+ G2.nodes["Z"]["label"] = G1.nodes[2]["label"]
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
+
+ # Add an extra edge
+ G1.nodes[0]["label"] = "blue"
+ G2.nodes["Z"]["label"] = "blue"
+ G1.add_edge(0, 1)
+
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
+
+ # Add extra edge to both
+ G2.add_edge("Z", "A")
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
+
+ def test_custom_graph3_same_labels(self):
+ G1 = nx.Graph()
+
+ mapped = {1: 9, 2: 8, 3: 7, 4: 6, 5: 3, 8: 5, 9: 4, 7: 1, 6: 2}
+ edges1 = [
+ (1, 2),
+ (1, 3),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (4, 7),
+ (4, 9),
+ (5, 8),
+ (8, 9),
+ (5, 6),
+ (6, 7),
+ (5, 2),
+ ]
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Connect nodes maintaining symmetry
+ G1.add_edges_from([(6, 9), (7, 8)])
+ G2.add_edges_from([(mapped[6], mapped[8]), (mapped[7], mapped[9])])
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
+
+ # Make isomorphic
+ G1.add_edges_from([(6, 8), (7, 9)])
+ G2.add_edges_from([(mapped[6], mapped[9]), (mapped[7], mapped[8])])
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Connect more nodes
+ G1.add_edges_from([(2, 7), (3, 6)])
+ G2.add_edges_from([(mapped[2], mapped[7]), (mapped[3], mapped[6])])
+ G1.add_node(10)
+ G2.add_node("Z")
+ G1.nodes[10]["label"] = "blue"
+ G2.nodes["Z"]["label"] = "blue"
+
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Connect the newly added node, to opposite sides of the graph
+ G1.add_edges_from([(10, 1), (10, 5), (10, 8)])
+ G2.add_edges_from([("Z", mapped[1]), ("Z", mapped[4]), ("Z", mapped[9])])
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Get two subgraphs that are not isomorphic but are easy to make
+ H1 = nx.Graph(G1.subgraph([2, 3, 4, 5, 6, 7, 10]))
+ H2 = nx.Graph(
+ G2.subgraph(
+ [mapped[4], mapped[5], mapped[6], mapped[7], mapped[8], mapped[9], "Z"]
+ )
+ )
+ assert vf2pp_isomorphism(H1, H2, node_label="label") is None
+
+ # Restructure both to make them isomorphic
+ H1.add_edges_from([(10, 2), (10, 6), (3, 6), (2, 7), (2, 6), (3, 7)])
+ H2.add_edges_from(
+ [("Z", mapped[7]), (mapped[6], mapped[9]), (mapped[7], mapped[8])]
+ )
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
+
+ # Add edges with opposite direction in each Graph
+ H1.add_edge(3, 5)
+ H2.add_edge(mapped[5], mapped[7])
+ assert vf2pp_isomorphism(H1, H2, node_label="label") is None
+
+ def test_custom_graph3_different_labels(self):
+ G1 = nx.Graph()
+
+ mapped = {1: 9, 2: 8, 3: 7, 4: 6, 5: 3, 8: 5, 9: 4, 7: 1, 6: 2}
+ edges1 = [
+ (1, 2),
+ (1, 3),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (4, 7),
+ (4, 9),
+ (5, 8),
+ (8, 9),
+ (5, 6),
+ (6, 7),
+ (5, 2),
+ ]
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
+
+ # Add extra edge to G1
+ G1.add_edge(1, 7)
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
+
+ # Compensate in G2
+ G2.add_edge(9, 1)
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
+
+ # Add extra node
+ G1.add_node("A")
+ G2.add_node("K")
+ G1.nodes["A"]["label"] = "green"
+ G2.nodes["K"]["label"] = "green"
+ mapped.update({"A": "K"})
+
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
+
+ # Connect A to one side of G1 and K to the opposite
+ G1.add_edge("A", 6)
+ G2.add_edge("K", 5)
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
+
+ # Make the graphs symmetrical
+ G1.add_edge(1, 5)
+ G1.add_edge(2, 9)
+ G2.add_edge(9, 3)
+ G2.add_edge(8, 4)
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
+
+ # Assign same colors so the two opposite sides are identical
+ for node in G1.nodes():
+ color = "red"
+ G1.nodes[node]["label"] = color
+ G2.nodes[mapped[node]]["label"] = color
+
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ def test_custom_graph4_different_labels(self):
+ G1 = nx.Graph()
+ edges1 = [
+ (1, 2),
+ (2, 3),
+ (3, 8),
+ (3, 4),
+ (4, 5),
+ (4, 6),
+ (3, 6),
+ (8, 7),
+ (8, 9),
+ (5, 9),
+ (10, 11),
+ (11, 12),
+ (12, 13),
+ (11, 13),
+ ]
+
+ mapped = {
+ 1: "n",
+ 2: "m",
+ 3: "l",
+ 4: "j",
+ 5: "k",
+ 6: "i",
+ 7: "g",
+ 8: "h",
+ 9: "f",
+ 10: "b",
+ 11: "a",
+ 12: "d",
+ 13: "e",
+ }
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
+
+ def test_custom_graph4_same_labels(self):
+ G1 = nx.Graph()
+ edges1 = [
+ (1, 2),
+ (2, 3),
+ (3, 8),
+ (3, 4),
+ (4, 5),
+ (4, 6),
+ (3, 6),
+ (8, 7),
+ (8, 9),
+ (5, 9),
+ (10, 11),
+ (11, 12),
+ (12, 13),
+ (11, 13),
+ ]
+
+ mapped = {
+ 1: "n",
+ 2: "m",
+ 3: "l",
+ 4: "j",
+ 5: "k",
+ 6: "i",
+ 7: "g",
+ 8: "h",
+ 9: "f",
+ 10: "b",
+ 11: "a",
+ 12: "d",
+ 13: "e",
+ }
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Add nodes of different label
+ G1.add_node(0)
+ G2.add_node("z")
+ G1.nodes[0]["label"] = "green"
+ G2.nodes["z"]["label"] = "blue"
+
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
+
+ # Make the labels identical
+ G2.nodes["z"]["label"] = "green"
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Change the structure of the graphs, keeping them isomorphic
+ G1.add_edge(2, 5)
+ G2.remove_edge("i", "l")
+ G2.add_edge("g", "l")
+ G2.add_edge("m", "f")
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Change the structure of the disconnected sub-graph, keeping it isomorphic
+ G1.remove_node(13)
+ G2.remove_node("d")
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Connect the newly added node to the disconnected graph, which now is just a path of size 3
+ G1.add_edge(0, 10)
+ G2.add_edge("e", "z")
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Connect the two disconnected sub-graphs, forming a single graph
+ G1.add_edge(11, 3)
+ G1.add_edge(0, 8)
+ G2.add_edge("a", "l")
+ G2.add_edge("z", "j")
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ def test_custom_graph5_same_labels(self):
+ G1 = nx.Graph()
+ edges1 = [
+ (1, 5),
+ (1, 2),
+ (1, 4),
+ (2, 3),
+ (2, 6),
+ (3, 4),
+ (3, 7),
+ (4, 8),
+ (5, 8),
+ (5, 6),
+ (6, 7),
+ (7, 8),
+ ]
+ mapped = {1: "a", 2: "h", 3: "d", 4: "i", 5: "g", 6: "b", 7: "j", 8: "c"}
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Add different edges in each graph, maintaining symmetry
+ G1.add_edges_from([(3, 6), (2, 7), (2, 5), (1, 3), (4, 7), (6, 8)])
+ G2.add_edges_from(
+ [
+ (mapped[6], mapped[3]),
+ (mapped[2], mapped[7]),
+ (mapped[1], mapped[6]),
+ (mapped[5], mapped[7]),
+ (mapped[3], mapped[8]),
+ (mapped[2], mapped[4]),
+ ]
+ )
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ # Obtain two different but isomorphic subgraphs from G1 and G2
+ H1 = nx.Graph(G1.subgraph([1, 5, 8, 6, 7, 3]))
+ H2 = nx.Graph(
+ G2.subgraph(
+ [mapped[1], mapped[4], mapped[8], mapped[7], mapped[3], mapped[5]]
+ )
+ )
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
+
+ # Delete corresponding node from the two graphs
+ H1.remove_node(8)
+ H2.remove_node(mapped[7])
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
+
+ # Re-orient, maintaining isomorphism
+ H1.add_edge(1, 6)
+ H1.remove_edge(3, 6)
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
+
+ def test_custom_graph5_different_labels(self):
+ G1 = nx.Graph()
+ edges1 = [
+ (1, 5),
+ (1, 2),
+ (1, 4),
+ (2, 3),
+ (2, 6),
+ (3, 4),
+ (3, 7),
+ (4, 8),
+ (5, 8),
+ (5, 6),
+ (6, 7),
+ (7, 8),
+ ]
+ mapped = {1: "a", 2: "h", 3: "d", 4: "i", 5: "g", 6: "b", 7: "j", 8: "c"}
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ colors = ["red", "blue", "grey", "none", "brown", "solarized", "yellow", "pink"]
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
+
+ # Assign different colors to matching nodes
+ c = 0
+ for node in G1.nodes():
+ color1 = colors[c]
+ color2 = colors[(c + 3) % len(colors)]
+ G1.nodes[node]["label"] = color1
+ G2.nodes[mapped[node]]["label"] = color2
+ c += 1
+
+ assert vf2pp_isomorphism(G1, G2, node_label="label") is None
+
+ # Get symmetrical sub-graphs of G1,G2 and compare them
+ H1 = G1.subgraph([1, 5])
+ H2 = G2.subgraph(["i", "c"])
+ c = 0
+ for node1, node2 in zip(H1.nodes(), H2.nodes()):
+ H1.nodes[node1]["label"] = "red"
+ H2.nodes[node2]["label"] = "red"
+ c += 1
+
+ assert vf2pp_isomorphism(H1, H2, node_label="label")
+
+ def test_disconnected_graph_all_same_labels(self):
+ G1 = nx.Graph()
+ G1.add_nodes_from(list(range(10)))
+
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+ def test_disconnected_graph_all_different_labels(self):
+ G1 = nx.Graph()
+ G1.add_nodes_from(list(range(10)))
+
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ assert vf2pp_isomorphism(G1, G2, node_label="label") == mapped
+
+ def test_disconnected_graph_some_same_labels(self):
+ G1 = nx.Graph()
+ G1.add_nodes_from(list(range(10)))
+
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ colors = [
+ "white",
+ "white",
+ "white",
+ "purple",
+ "purple",
+ "red",
+ "red",
+ "pink",
+ "pink",
+ "pink",
+ ]
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(colors))), "label")
+ nx.set_node_attributes(
+ G2, dict(zip([mapped[n] for n in G1], it.cycle(colors))), "label"
+ )
+
+ assert vf2pp_isomorphism(G1, G2, node_label="label")
+
+
+class TestMultiGraphISOVF2pp:
+ def test_custom_multigraph1_same_labels(self):
+ G1 = nx.MultiGraph()
+
+ mapped = {1: "A", 2: "B", 3: "C", 4: "D", 5: "Z", 6: "E"}
+ edges1 = [
+ (1, 2),
+ (1, 3),
+ (1, 4),
+ (1, 4),
+ (1, 4),
+ (2, 3),
+ (2, 6),
+ (2, 6),
+ (3, 4),
+ (3, 4),
+ (5, 1),
+ (5, 1),
+ (5, 2),
+ (5, 2),
+ ]
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Transfer the 2-clique to the right side of G1
+ G1.remove_edges_from([(2, 6), (2, 6)])
+ G1.add_edges_from([(3, 6), (3, 6)])
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Delete an edges, making them symmetrical, so the position of the 2-clique doesn't matter
+ G2.remove_edge(mapped[1], mapped[4])
+ G1.remove_edge(1, 4)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Add self-loops
+ G1.add_edges_from([(5, 5), (5, 5), (1, 1)])
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Compensate in G2
+ G2.add_edges_from(
+ [(mapped[1], mapped[1]), (mapped[4], mapped[4]), (mapped[4], mapped[4])]
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ def test_custom_multigraph1_different_labels(self):
+ G1 = nx.MultiGraph()
+
+ mapped = {1: "A", 2: "B", 3: "C", 4: "D", 5: "Z", 6: "E"}
+ edges1 = [
+ (1, 2),
+ (1, 3),
+ (1, 4),
+ (1, 4),
+ (1, 4),
+ (2, 3),
+ (2, 6),
+ (2, 6),
+ (3, 4),
+ (3, 4),
+ (5, 1),
+ (5, 1),
+ (5, 2),
+ (5, 2),
+ ]
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+ assert m == mapped
+
+ # Re-structure G1, maintaining the degree sequence
+ G1.remove_edge(1, 4)
+ G1.add_edge(1, 5)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Restructure G2, making it isomorphic to G1
+ G2.remove_edge("A", "D")
+ G2.add_edge("A", "Z")
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+ assert m == mapped
+
+ # Add edge from node to itself
+ G1.add_edges_from([(6, 6), (6, 6), (6, 6)])
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Same for G2
+ G2.add_edges_from([("E", "E"), ("E", "E"), ("E", "E")])
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+ assert m == mapped
+
+ def test_custom_multigraph2_same_labels(self):
+ G1 = nx.MultiGraph()
+
+ mapped = {1: "A", 2: "C", 3: "D", 4: "E", 5: "G", 7: "B", 6: "F"}
+ edges1 = [
+ (1, 2),
+ (1, 2),
+ (1, 5),
+ (1, 5),
+ (1, 5),
+ (5, 6),
+ (2, 3),
+ (2, 3),
+ (2, 4),
+ (3, 4),
+ (3, 4),
+ (4, 5),
+ (4, 5),
+ (4, 5),
+ (2, 7),
+ (2, 7),
+ (2, 7),
+ ]
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Obtain two non-isomorphic subgraphs from the graph
+ G2.remove_edges_from([(mapped[1], mapped[2]), (mapped[1], mapped[2])])
+ G2.add_edge(mapped[1], mapped[4])
+ H1 = nx.MultiGraph(G1.subgraph([2, 3, 4, 7]))
+ H2 = nx.MultiGraph(G2.subgraph([mapped[1], mapped[4], mapped[5], mapped[6]]))
+
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert not m
+
+ # Make them isomorphic
+ H1.remove_edge(3, 4)
+ H1.add_edges_from([(2, 3), (2, 4), (2, 4)])
+ H2.add_edges_from([(mapped[5], mapped[6]), (mapped[5], mapped[6])])
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Remove triangle edge
+ H1.remove_edges_from([(2, 3), (2, 3), (2, 3)])
+ H2.remove_edges_from([(mapped[5], mapped[4])] * 3)
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Change the edge orientation such that H1 is rotated H2
+ H1.remove_edges_from([(2, 7), (2, 7)])
+ H1.add_edges_from([(3, 4), (3, 4)])
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Add extra edges maintaining degree sequence, but in a non-symmetrical manner
+ H2.add_edge(mapped[5], mapped[1])
+ H1.add_edge(3, 4)
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert not m
+
+ def test_custom_multigraph2_different_labels(self):
+ G1 = nx.MultiGraph()
+
+ mapped = {1: "A", 2: "C", 3: "D", 4: "E", 5: "G", 7: "B", 6: "F"}
+ edges1 = [
+ (1, 2),
+ (1, 2),
+ (1, 5),
+ (1, 5),
+ (1, 5),
+ (5, 6),
+ (2, 3),
+ (2, 3),
+ (2, 4),
+ (3, 4),
+ (3, 4),
+ (4, 5),
+ (4, 5),
+ (4, 5),
+ (2, 7),
+ (2, 7),
+ (2, 7),
+ ]
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+ assert m == mapped
+
+ # Re-structure G1
+ G1.remove_edge(2, 7)
+ G1.add_edge(5, 6)
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Same for G2
+ G2.remove_edge("B", "C")
+ G2.add_edge("G", "F")
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+ assert m == mapped
+
+ # Delete node from G1 and G2, keeping them isomorphic
+ G1.remove_node(3)
+ G2.remove_node("D")
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Change G1 edges
+ G1.remove_edge(1, 2)
+ G1.remove_edge(2, 7)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Make G2 identical to G1, but with different edge orientation and different labels
+ G2.add_edges_from([("A", "C"), ("C", "E"), ("C", "E")])
+ G2.remove_edges_from(
+ [("A", "G"), ("A", "G"), ("F", "G"), ("E", "G"), ("E", "G")]
+ )
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Make all labels the same, so G1 and G2 are also isomorphic
+ for n1, n2 in zip(G1.nodes(), G2.nodes()):
+ G1.nodes[n1]["label"] = "blue"
+ G2.nodes[n2]["label"] = "blue"
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ def test_custom_multigraph3_same_labels(self):
+ G1 = nx.MultiGraph()
+
+ mapped = {1: 9, 2: 8, 3: 7, 4: 6, 5: 3, 8: 5, 9: 4, 7: 1, 6: 2}
+ edges1 = [
+ (1, 2),
+ (1, 3),
+ (1, 3),
+ (2, 3),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (4, 7),
+ (4, 9),
+ (4, 9),
+ (4, 9),
+ (5, 8),
+ (5, 8),
+ (8, 9),
+ (8, 9),
+ (5, 6),
+ (6, 7),
+ (6, 7),
+ (6, 7),
+ (5, 2),
+ ]
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Connect nodes maintaining symmetry
+ G1.add_edges_from([(6, 9), (7, 8), (5, 8), (4, 9), (4, 9)])
+ G2.add_edges_from(
+ [
+ (mapped[6], mapped[8]),
+ (mapped[7], mapped[9]),
+ (mapped[5], mapped[8]),
+ (mapped[4], mapped[9]),
+ (mapped[4], mapped[9]),
+ ]
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Make isomorphic
+ G1.add_edges_from([(6, 8), (6, 8), (7, 9), (7, 9), (7, 9)])
+ G2.add_edges_from(
+ [
+ (mapped[6], mapped[8]),
+ (mapped[6], mapped[9]),
+ (mapped[7], mapped[8]),
+ (mapped[7], mapped[9]),
+ (mapped[7], mapped[9]),
+ ]
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Connect more nodes
+ G1.add_edges_from([(2, 7), (2, 7), (3, 6), (3, 6)])
+ G2.add_edges_from(
+ [
+ (mapped[2], mapped[7]),
+ (mapped[2], mapped[7]),
+ (mapped[3], mapped[6]),
+ (mapped[3], mapped[6]),
+ ]
+ )
+ G1.add_node(10)
+ G2.add_node("Z")
+ G1.nodes[10]["label"] = "blue"
+ G2.nodes["Z"]["label"] = "blue"
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Connect the newly added node, to opposite sides of the graph
+ G1.add_edges_from([(10, 1), (10, 5), (10, 8), (10, 10), (10, 10)])
+ G2.add_edges_from(
+ [
+ ("Z", mapped[1]),
+ ("Z", mapped[4]),
+ ("Z", mapped[9]),
+ ("Z", "Z"),
+ ("Z", "Z"),
+ ]
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # We connected the new node to opposite sides, so G1 must be symmetrical to G2. Re-structure them to be so
+ G1.remove_edges_from([(1, 3), (4, 9), (4, 9), (7, 9)])
+ G2.remove_edges_from(
+ [
+ (mapped[1], mapped[3]),
+ (mapped[4], mapped[9]),
+ (mapped[4], mapped[9]),
+ (mapped[7], mapped[9]),
+ ]
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Get two subgraphs that are not isomorphic but are easy to make
+ H1 = nx.Graph(G1.subgraph([2, 3, 4, 5, 6, 7, 10]))
+ H2 = nx.Graph(
+ G2.subgraph(
+ [mapped[4], mapped[5], mapped[6], mapped[7], mapped[8], mapped[9], "Z"]
+ )
+ )
+
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert not m
+
+ # Restructure both to make them isomorphic
+ H1.add_edges_from([(10, 2), (10, 6), (3, 6), (2, 7), (2, 6), (3, 7)])
+ H2.add_edges_from(
+ [("Z", mapped[7]), (mapped[6], mapped[9]), (mapped[7], mapped[8])]
+ )
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Remove one self-loop in H2
+ H2.remove_edge("Z", "Z")
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert not m
+
+ # Compensate in H1
+ H1.remove_edge(10, 10)
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ def test_custom_multigraph3_different_labels(self):
+ G1 = nx.MultiGraph()
+
+ mapped = {1: 9, 2: 8, 3: 7, 4: 6, 5: 3, 8: 5, 9: 4, 7: 1, 6: 2}
+ edges1 = [
+ (1, 2),
+ (1, 3),
+ (1, 3),
+ (2, 3),
+ (2, 3),
+ (3, 4),
+ (4, 5),
+ (4, 7),
+ (4, 9),
+ (4, 9),
+ (4, 9),
+ (5, 8),
+ (5, 8),
+ (8, 9),
+ (8, 9),
+ (5, 6),
+ (6, 7),
+ (6, 7),
+ (6, 7),
+ (5, 2),
+ ]
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+ assert m == mapped
+
+ # Delete edge maintaining isomorphism
+ G1.remove_edge(4, 9)
+ G2.remove_edge(4, 6)
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+ assert m == mapped
+
+ # Change edge orientation such that G1 mirrors G2
+ G1.add_edges_from([(4, 9), (1, 2), (1, 2)])
+ G1.remove_edges_from([(1, 3), (1, 3)])
+ G2.add_edges_from([(3, 5), (7, 9)])
+ G2.remove_edge(8, 9)
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Make all labels the same, so G1 and G2 are also isomorphic
+ for n1, n2 in zip(G1.nodes(), G2.nodes()):
+ G1.nodes[n1]["label"] = "blue"
+ G2.nodes[n2]["label"] = "blue"
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ G1.add_node(10)
+ G2.add_node("Z")
+ G1.nodes[10]["label"] = "green"
+ G2.nodes["Z"]["label"] = "green"
+
+ # Add different number of edges between the new nodes and themselves
+ G1.add_edges_from([(10, 10), (10, 10)])
+ G2.add_edges_from([("Z", "Z")])
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Make the number of self-edges equal
+ G1.remove_edge(10, 10)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Connect the new node to the graph
+ G1.add_edges_from([(10, 3), (10, 4)])
+ G2.add_edges_from([("Z", 8), ("Z", 3)])
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Remove central node
+ G1.remove_node(4)
+ G2.remove_node(3)
+ G1.add_edges_from([(5, 6), (5, 6), (5, 7)])
+ G2.add_edges_from([(1, 6), (1, 6), (6, 2)])
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ def test_custom_multigraph4_same_labels(self):
+ G1 = nx.MultiGraph()
+ edges1 = [
+ (1, 2),
+ (1, 2),
+ (2, 2),
+ (2, 3),
+ (3, 8),
+ (3, 8),
+ (3, 4),
+ (4, 5),
+ (4, 5),
+ (4, 5),
+ (4, 6),
+ (3, 6),
+ (3, 6),
+ (6, 6),
+ (8, 7),
+ (7, 7),
+ (8, 9),
+ (9, 9),
+ (8, 9),
+ (8, 9),
+ (5, 9),
+ (10, 11),
+ (11, 12),
+ (12, 13),
+ (11, 13),
+ (10, 10),
+ (10, 11),
+ (11, 13),
+ ]
+
+ mapped = {
+ 1: "n",
+ 2: "m",
+ 3: "l",
+ 4: "j",
+ 5: "k",
+ 6: "i",
+ 7: "g",
+ 8: "h",
+ 9: "f",
+ 10: "b",
+ 11: "a",
+ 12: "d",
+ 13: "e",
+ }
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Add extra but corresponding edges to both graphs
+ G1.add_edges_from([(2, 2), (2, 3), (2, 8), (3, 4)])
+ G2.add_edges_from([("m", "m"), ("m", "l"), ("m", "h"), ("l", "j")])
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Obtain subgraphs
+ H1 = nx.MultiGraph(G1.subgraph([2, 3, 4, 6, 10, 11, 12, 13]))
+ H2 = nx.MultiGraph(
+ G2.subgraph(
+ [
+ mapped[2],
+ mapped[3],
+ mapped[8],
+ mapped[9],
+ mapped[10],
+ mapped[11],
+ mapped[12],
+ mapped[13],
+ ]
+ )
+ )
+
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert not m
+
+ # Make them isomorphic
+ H2.remove_edges_from(
+ [(mapped[3], mapped[2]), (mapped[9], mapped[8]), (mapped[2], mapped[2])]
+ )
+ H2.add_edges_from([(mapped[9], mapped[9]), (mapped[2], mapped[8])])
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Re-structure the disconnected sub-graph
+ H1.remove_node(12)
+ H2.remove_node(mapped[12])
+ H1.add_edge(13, 13)
+ H2.add_edge(mapped[13], mapped[13])
+
+ # Connect the two disconnected components, forming a single graph
+ H1.add_edges_from([(3, 13), (6, 11)])
+ H2.add_edges_from([(mapped[8], mapped[10]), (mapped[2], mapped[11])])
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Change orientation of self-loops in one graph, maintaining the degree sequence
+ H1.remove_edges_from([(2, 2), (3, 6)])
+ H1.add_edges_from([(6, 6), (2, 3)])
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert not m
+
+ def test_custom_multigraph4_different_labels(self):
+ G1 = nx.MultiGraph()
+ edges1 = [
+ (1, 2),
+ (1, 2),
+ (2, 2),
+ (2, 3),
+ (3, 8),
+ (3, 8),
+ (3, 4),
+ (4, 5),
+ (4, 5),
+ (4, 5),
+ (4, 6),
+ (3, 6),
+ (3, 6),
+ (6, 6),
+ (8, 7),
+ (7, 7),
+ (8, 9),
+ (9, 9),
+ (8, 9),
+ (8, 9),
+ (5, 9),
+ (10, 11),
+ (11, 12),
+ (12, 13),
+ (11, 13),
+ ]
+
+ mapped = {
+ 1: "n",
+ 2: "m",
+ 3: "l",
+ 4: "j",
+ 5: "k",
+ 6: "i",
+ 7: "g",
+ 8: "h",
+ 9: "f",
+ 10: "b",
+ 11: "a",
+ 12: "d",
+ 13: "e",
+ }
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m == mapped
+
+ # Add extra but corresponding edges to both graphs
+ G1.add_edges_from([(2, 2), (2, 3), (2, 8), (3, 4)])
+ G2.add_edges_from([("m", "m"), ("m", "l"), ("m", "h"), ("l", "j")])
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m == mapped
+
+ # Obtain isomorphic subgraphs
+ H1 = nx.MultiGraph(G1.subgraph([2, 3, 4, 6]))
+ H2 = nx.MultiGraph(G2.subgraph(["m", "l", "j", "i"]))
+
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Delete the 3-clique, keeping only the path-graph. Also, H1 mirrors H2
+ H1.remove_node(4)
+ H2.remove_node("j")
+ H1.remove_edges_from([(2, 2), (2, 3), (6, 6)])
+ H2.remove_edges_from([("l", "i"), ("m", "m"), ("m", "m")])
+
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert not m
+
+ # Assign the same labels so that mirroring means isomorphic
+ for n1, n2 in zip(H1.nodes(), H2.nodes()):
+ H1.nodes[n1]["label"] = "red"
+ H2.nodes[n2]["label"] = "red"
+
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Leave only one node with self-loop
+ H1.remove_nodes_from([3, 6])
+ H2.remove_nodes_from(["m", "l"])
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Remove one self-loop from H1
+ H1.remove_edge(2, 2)
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert not m
+
+ # Same for H2
+ H2.remove_edge("i", "i")
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Compose H1 with the disconnected sub-graph of G1. Same for H2
+ S1 = nx.compose(H1, nx.MultiGraph(G1.subgraph([10, 11, 12, 13])))
+ S2 = nx.compose(H2, nx.MultiGraph(G2.subgraph(["a", "b", "d", "e"])))
+
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ # Connect the two components
+ S1.add_edges_from([(13, 13), (13, 13), (2, 13)])
+ S2.add_edges_from([("a", "a"), ("a", "a"), ("i", "e")])
+ m = vf2pp_isomorphism(H1, H2, node_label="label")
+ assert m
+
+ def test_custom_multigraph5_same_labels(self):
+ G1 = nx.MultiGraph()
+
+ edges1 = [
+ (1, 5),
+ (1, 2),
+ (1, 4),
+ (2, 3),
+ (2, 6),
+ (3, 4),
+ (3, 7),
+ (4, 8),
+ (5, 8),
+ (5, 6),
+ (6, 7),
+ (7, 8),
+ ]
+ mapped = {1: "a", 2: "h", 3: "d", 4: "i", 5: "g", 6: "b", 7: "j", 8: "c"}
+
+ G1.add_edges_from(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Add multiple edges and self-loops, maintaining isomorphism
+ G1.add_edges_from(
+ [(1, 2), (1, 2), (3, 7), (8, 8), (8, 8), (7, 8), (2, 3), (5, 6)]
+ )
+ G2.add_edges_from(
+ [
+ ("a", "h"),
+ ("a", "h"),
+ ("d", "j"),
+ ("c", "c"),
+ ("c", "c"),
+ ("j", "c"),
+ ("d", "h"),
+ ("g", "b"),
+ ]
+ )
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Make G2 to be the rotated G1
+ G2.remove_edges_from(
+ [
+ ("a", "h"),
+ ("a", "h"),
+ ("d", "j"),
+ ("c", "c"),
+ ("c", "c"),
+ ("j", "c"),
+ ("d", "h"),
+ ("g", "b"),
+ ]
+ )
+ G2.add_edges_from(
+ [
+ ("d", "i"),
+ ("a", "h"),
+ ("g", "b"),
+ ("g", "b"),
+ ("i", "i"),
+ ("i", "i"),
+ ("b", "j"),
+ ("d", "j"),
+ ]
+ )
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ def test_disconnected_multigraph_all_same_labels(self):
+ G1 = nx.MultiGraph()
+ G1.add_nodes_from(list(range(10)))
+ G1.add_edges_from([(i, i) for i in range(10)])
+
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_same))), "label")
+ nx.set_node_attributes(G2, dict(zip(G2, it.cycle(labels_same))), "label")
+
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Add self-loops to non-mapped nodes. Should be the same, as the graph is disconnected.
+ G1.add_edges_from([(i, i) for i in range(5, 8)] * 3)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Compensate in G2
+ G2.add_edges_from([(i, i) for i in range(3)] * 3)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ # Add one more self-loop in G2
+ G2.add_edges_from([(0, 0), (1, 1), (1, 1)])
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Compensate in G1
+ G1.add_edges_from([(5, 5), (7, 7), (7, 7)])
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+ def test_disconnected_multigraph_all_different_labels(self):
+ G1 = nx.MultiGraph()
+ G1.add_nodes_from(list(range(10)))
+ G1.add_edges_from([(i, i) for i in range(10)])
+
+ mapped = {0: 9, 1: 8, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1, 9: 0}
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip([mapped[n] for n in G1], it.cycle(labels_many))),
+ "label",
+ )
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+ assert m == mapped
+
+ # Add self-loops to non-mapped nodes. Now it is not the same, as there are different labels
+ G1.add_edges_from([(i, i) for i in range(5, 8)] * 3)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Add self-loops to non mapped nodes in G2 as well
+ G2.add_edges_from([(mapped[i], mapped[i]) for i in range(3)] * 7)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Add self-loops to mapped nodes in G2
+ G2.add_edges_from([(mapped[i], mapped[i]) for i in range(5, 8)] * 3)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert not m
+
+ # Add self-loops to G1 so that they are even in both graphs
+ G1.add_edges_from([(i, i) for i in range(3)] * 7)
+ m = vf2pp_isomorphism(G1, G2, node_label="label")
+ assert m
+
+
+class TestDiGraphISOVF2pp:
+ def test_wikipedia_graph(self):
+ edges1 = [
+ (1, 5),
+ (1, 2),
+ (1, 4),
+ (3, 2),
+ (6, 2),
+ (3, 4),
+ (7, 3),
+ (4, 8),
+ (5, 8),
+ (6, 5),
+ (6, 7),
+ (7, 8),
+ ]
+ mapped = {1: "a", 2: "h", 3: "d", 4: "i", 5: "g", 6: "b", 7: "j", 8: "c"}
+
+ G1 = nx.DiGraph(edges1)
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ assert vf2pp_isomorphism(G1, G2) == mapped
+
+ # Change the direction of an edge
+ G1.remove_edge(1, 5)
+ G1.add_edge(5, 1)
+ assert vf2pp_isomorphism(G1, G2) is None
+
+ def test_non_isomorphic_same_degree_sequence(self):
+ r"""
+ G1 G2
+ x--------------x x--------------x
+ | \ | | \ |
+ | x-------x | | x-------x |
+ | | | | | | | |
+ | x-------x | | x-------x |
+ | / | | \ |
+ x--------------x x--------------x
+ """
+ edges1 = [
+ (1, 5),
+ (1, 2),
+ (4, 1),
+ (3, 2),
+ (3, 4),
+ (4, 8),
+ (5, 8),
+ (6, 5),
+ (6, 7),
+ (7, 8),
+ ]
+ edges2 = [
+ (1, 5),
+ (1, 2),
+ (4, 1),
+ (3, 2),
+ (4, 3),
+ (5, 8),
+ (6, 5),
+ (6, 7),
+ (3, 7),
+ (8, 7),
+ ]
+
+ G1 = nx.DiGraph(edges1)
+ G2 = nx.DiGraph(edges2)
+ assert vf2pp_isomorphism(G1, G2) is None
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py
new file mode 100644
index 00000000..0e29b1be
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py
@@ -0,0 +1,3106 @@
+import itertools as it
+
+import pytest
+
+import networkx as nx
+from networkx import vf2pp_is_isomorphic, vf2pp_isomorphism
+from networkx.algorithms.isomorphism.vf2pp import (
+ _consistent_PT,
+ _cut_PT,
+ _feasibility,
+ _find_candidates,
+ _find_candidates_Di,
+ _GraphParameters,
+ _initialize_parameters,
+ _matching_order,
+ _restore_Tinout,
+ _restore_Tinout_Di,
+ _StateParameters,
+ _update_Tinout,
+)
+
+labels_same = ["blue"]
+
+labels_many = [
+ "white",
+ "red",
+ "blue",
+ "green",
+ "orange",
+ "black",
+ "purple",
+ "yellow",
+ "brown",
+ "cyan",
+ "solarized",
+ "pink",
+ "none",
+]
+
+
+class TestNodeOrdering:
+ def test_empty_graph(self):
+ G1 = nx.Graph()
+ G2 = nx.Graph()
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ assert len(set(_matching_order(gparams))) == 0
+
+ def test_single_node(self):
+ G1 = nx.Graph()
+ G2 = nx.Graph()
+ G1.add_node(1)
+ G2.add_node(1)
+
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels_many))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip(G2, it.cycle(labels_many))),
+ "label",
+ )
+ l1, l2 = (
+ nx.get_node_attributes(G1, "label"),
+ nx.get_node_attributes(G2, "label"),
+ )
+
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+ m = _matching_order(gparams)
+ assert m == [1]
+
+ def test_matching_order(self):
+ labels = [
+ "blue",
+ "blue",
+ "red",
+ "red",
+ "red",
+ "red",
+ "green",
+ "green",
+ "green",
+ "yellow",
+ "purple",
+ "purple",
+ "blue",
+ "blue",
+ ]
+ G1 = nx.Graph(
+ [
+ (0, 1),
+ (0, 2),
+ (1, 2),
+ (2, 5),
+ (2, 4),
+ (1, 3),
+ (1, 4),
+ (3, 6),
+ (4, 6),
+ (6, 7),
+ (7, 8),
+ (9, 10),
+ (9, 11),
+ (11, 12),
+ (11, 13),
+ (12, 13),
+ (10, 13),
+ ]
+ )
+ G2 = G1.copy()
+ nx.set_node_attributes(G1, dict(zip(G1, it.cycle(labels))), "label")
+ nx.set_node_attributes(
+ G2,
+ dict(zip(G2, it.cycle(labels))),
+ "label",
+ )
+ l1, l2 = (
+ nx.get_node_attributes(G1, "label"),
+ nx.get_node_attributes(G2, "label"),
+ )
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+
+ expected = [9, 11, 10, 13, 12, 1, 2, 4, 0, 3, 6, 5, 7, 8]
+ assert _matching_order(gparams) == expected
+
+ def test_matching_order_all_branches(self):
+ G1 = nx.Graph(
+ [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (2, 4), (3, 4)]
+ )
+ G1.add_node(5)
+ G2 = G1.copy()
+
+ G1.nodes[0]["label"] = "black"
+ G1.nodes[1]["label"] = "blue"
+ G1.nodes[2]["label"] = "blue"
+ G1.nodes[3]["label"] = "red"
+ G1.nodes[4]["label"] = "red"
+ G1.nodes[5]["label"] = "blue"
+
+ G2.nodes[0]["label"] = "black"
+ G2.nodes[1]["label"] = "blue"
+ G2.nodes[2]["label"] = "blue"
+ G2.nodes[3]["label"] = "red"
+ G2.nodes[4]["label"] = "red"
+ G2.nodes[5]["label"] = "blue"
+
+ l1, l2 = (
+ nx.get_node_attributes(G1, "label"),
+ nx.get_node_attributes(G2, "label"),
+ )
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+
+ expected = [0, 4, 1, 3, 2, 5]
+ assert _matching_order(gparams) == expected
+
+
+class TestGraphCandidateSelection:
+ G1_edges = [
+ (1, 2),
+ (1, 4),
+ (1, 5),
+ (2, 3),
+ (2, 4),
+ (3, 4),
+ (4, 5),
+ (1, 6),
+ (6, 7),
+ (6, 8),
+ (8, 9),
+ (7, 9),
+ ]
+ mapped = {
+ 0: "x",
+ 1: "a",
+ 2: "b",
+ 3: "c",
+ 4: "d",
+ 5: "e",
+ 6: "f",
+ 7: "g",
+ 8: "h",
+ 9: "i",
+ }
+
+ def test_no_covered_neighbors_no_labels(self):
+ G1 = nx.Graph()
+ G1.add_edges_from(self.G1_edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, self.mapped)
+
+ G1_degree = dict(G1.degree)
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+
+ m = {9: self.mapped[9], 1: self.mapped[1]}
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
+
+ T1 = {7, 8, 2, 4, 5}
+ T1_tilde = {0, 3, 6}
+ T2 = {"g", "h", "b", "d", "e"}
+ T2_tilde = {"x", "c", "f"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
+ )
+
+ u = 3
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ u = 0
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ m.pop(9)
+ m_rev.pop(self.mapped[9])
+
+ T1 = {2, 4, 5, 6}
+ T1_tilde = {0, 3, 7, 8, 9}
+ T2 = {"g", "h", "b", "d", "e", "f"}
+ T2_tilde = {"x", "c", "g", "h", "i"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
+ )
+
+ u = 7
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {
+ self.mapped[u],
+ self.mapped[8],
+ self.mapped[3],
+ self.mapped[9],
+ }
+
+ def test_no_covered_neighbors_with_labels(self):
+ G1 = nx.Graph()
+ G1.add_edges_from(self.G1_edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, self.mapped)
+
+ G1_degree = dict(G1.degree)
+ nx.set_node_attributes(
+ G1,
+ dict(zip(G1, it.cycle(labels_many))),
+ "label",
+ )
+ nx.set_node_attributes(
+ G2,
+ dict(
+ zip(
+ [self.mapped[n] for n in G1],
+ it.cycle(labels_many),
+ )
+ ),
+ "label",
+ )
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+
+ m = {9: self.mapped[9], 1: self.mapped[1]}
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
+
+ T1 = {7, 8, 2, 4, 5, 6}
+ T1_tilde = {0, 3}
+ T2 = {"g", "h", "b", "d", "e", "f"}
+ T2_tilde = {"x", "c"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
+ )
+
+ u = 3
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ u = 0
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ # Change label of disconnected node
+ G1.nodes[u]["label"] = "blue"
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+
+ # No candidate
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == set()
+
+ m.pop(9)
+ m_rev.pop(self.mapped[9])
+
+ T1 = {2, 4, 5, 6}
+ T1_tilde = {0, 3, 7, 8, 9}
+ T2 = {"b", "d", "e", "f"}
+ T2_tilde = {"x", "c", "g", "h", "i"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
+ )
+
+ u = 7
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ G1.nodes[8]["label"] = G1.nodes[7]["label"]
+ G2.nodes[self.mapped[8]]["label"] = G1.nodes[7]["label"]
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u], self.mapped[8]}
+
+ def test_covered_neighbors_no_labels(self):
+ G1 = nx.Graph()
+ G1.add_edges_from(self.G1_edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, self.mapped)
+
+ G1_degree = dict(G1.degree)
+ l1 = dict(G1.nodes(data=None, default=-1))
+ l2 = dict(G2.nodes(data=None, default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+
+ m = {9: self.mapped[9], 1: self.mapped[1]}
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
+
+ T1 = {7, 8, 2, 4, 5, 6}
+ T1_tilde = {0, 3}
+ T2 = {"g", "h", "b", "d", "e", "f"}
+ T2_tilde = {"x", "c"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
+ )
+
+ u = 5
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ u = 6
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u], self.mapped[2]}
+
+ def test_covered_neighbors_with_labels(self):
+ G1 = nx.Graph()
+ G1.add_edges_from(self.G1_edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, self.mapped)
+
+ G1_degree = dict(G1.degree)
+ nx.set_node_attributes(
+ G1,
+ dict(zip(G1, it.cycle(labels_many))),
+ "label",
+ )
+ nx.set_node_attributes(
+ G2,
+ dict(
+ zip(
+ [self.mapped[n] for n in G1],
+ it.cycle(labels_many),
+ )
+ ),
+ "label",
+ )
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+
+ m = {9: self.mapped[9], 1: self.mapped[1]}
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
+
+ T1 = {7, 8, 2, 4, 5, 6}
+ T1_tilde = {0, 3}
+ T2 = {"g", "h", "b", "d", "e", "f"}
+ T2_tilde = {"x", "c"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
+ )
+
+ u = 5
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ u = 6
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ # Assign to 2, the same label as 6
+ G1.nodes[2]["label"] = G1.nodes[u]["label"]
+ G2.nodes[self.mapped[2]]["label"] = G1.nodes[u]["label"]
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(dict(G2.degree())),
+ )
+
+ candidates = _find_candidates(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u], self.mapped[2]}
+
+
+class TestDiGraphCandidateSelection:
+ G1_edges = [
+ (1, 2),
+ (1, 4),
+ (5, 1),
+ (2, 3),
+ (4, 2),
+ (3, 4),
+ (4, 5),
+ (1, 6),
+ (6, 7),
+ (6, 8),
+ (8, 9),
+ (7, 9),
+ ]
+ mapped = {
+ 0: "x",
+ 1: "a",
+ 2: "b",
+ 3: "c",
+ 4: "d",
+ 5: "e",
+ 6: "f",
+ 7: "g",
+ 8: "h",
+ 9: "i",
+ }
+
+ def test_no_covered_neighbors_no_labels(self):
+ G1 = nx.DiGraph()
+ G1.add_edges_from(self.G1_edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, self.mapped)
+
+ G1_degree = {
+ n: (in_degree, out_degree)
+ for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
+ }
+
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ G2.in_degree(), G2.out_degree()
+ )
+ }
+ ),
+ )
+
+ m = {9: self.mapped[9], 1: self.mapped[1]}
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
+
+ T1_out = {2, 4, 6}
+ T1_in = {5, 7, 8}
+ T1_tilde = {0, 3}
+ T2_out = {"b", "d", "f"}
+ T2_in = {"e", "g", "h"}
+ T2_tilde = {"x", "c"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
+ )
+
+ u = 3
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ u = 0
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ m.pop(9)
+ m_rev.pop(self.mapped[9])
+
+ T1_out = {2, 4, 6}
+ T1_in = {5}
+ T1_tilde = {0, 3, 7, 8, 9}
+ T2_out = {"b", "d", "f"}
+ T2_in = {"e"}
+ T2_tilde = {"x", "c", "g", "h", "i"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
+ )
+
+ u = 7
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u], self.mapped[8], self.mapped[3]}
+
+ def test_no_covered_neighbors_with_labels(self):
+ G1 = nx.DiGraph()
+ G1.add_edges_from(self.G1_edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, self.mapped)
+
+ G1_degree = {
+ n: (in_degree, out_degree)
+ for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
+ }
+ nx.set_node_attributes(
+ G1,
+ dict(zip(G1, it.cycle(labels_many))),
+ "label",
+ )
+ nx.set_node_attributes(
+ G2,
+ dict(
+ zip(
+ [self.mapped[n] for n in G1],
+ it.cycle(labels_many),
+ )
+ ),
+ "label",
+ )
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ G2.in_degree(), G2.out_degree()
+ )
+ }
+ ),
+ )
+
+ m = {9: self.mapped[9], 1: self.mapped[1]}
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
+
+ T1_out = {2, 4, 6}
+ T1_in = {5, 7, 8}
+ T1_tilde = {0, 3}
+ T2_out = {"b", "d", "f"}
+ T2_in = {"e", "g", "h"}
+ T2_tilde = {"x", "c"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
+ )
+
+ u = 3
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ u = 0
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ # Change label of disconnected node
+ G1.nodes[u]["label"] = "blue"
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ G2.in_degree(), G2.out_degree()
+ )
+ }
+ ),
+ )
+
+ # No candidate
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == set()
+
+ m.pop(9)
+ m_rev.pop(self.mapped[9])
+
+ T1_out = {2, 4, 6}
+ T1_in = {5}
+ T1_tilde = {0, 3, 7, 8, 9}
+ T2_out = {"b", "d", "f"}
+ T2_in = {"e"}
+ T2_tilde = {"x", "c", "g", "h", "i"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
+ )
+
+ u = 7
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ G1.nodes[8]["label"] = G1.nodes[7]["label"]
+ G2.nodes[self.mapped[8]]["label"] = G1.nodes[7]["label"]
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ G2.in_degree(), G2.out_degree()
+ )
+ }
+ ),
+ )
+
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u], self.mapped[8]}
+
+ def test_covered_neighbors_no_labels(self):
+ G1 = nx.DiGraph()
+ G1.add_edges_from(self.G1_edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, self.mapped)
+
+ G1_degree = {
+ n: (in_degree, out_degree)
+ for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
+ }
+
+ l1 = dict(G1.nodes(data=None, default=-1))
+ l2 = dict(G2.nodes(data=None, default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ G2.in_degree(), G2.out_degree()
+ )
+ }
+ ),
+ )
+
+ m = {9: self.mapped[9], 1: self.mapped[1]}
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
+
+ T1_out = {2, 4, 6}
+ T1_in = {5, 7, 8}
+ T1_tilde = {0, 3}
+ T2_out = {"b", "d", "f"}
+ T2_in = {"e", "g", "h"}
+ T2_tilde = {"x", "c"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
+ )
+
+ u = 5
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ u = 6
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ # Change the direction of an edge to make the degree orientation same as first candidate of u.
+ G1.remove_edge(4, 2)
+ G1.add_edge(2, 4)
+ G2.remove_edge("d", "b")
+ G2.add_edge("b", "d")
+
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ G2.in_degree(), G2.out_degree()
+ )
+ }
+ ),
+ )
+
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u], self.mapped[2]}
+
+ def test_covered_neighbors_with_labels(self):
+ G1 = nx.DiGraph()
+ G1.add_edges_from(self.G1_edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, self.mapped)
+
+ G1.remove_edge(4, 2)
+ G1.add_edge(2, 4)
+ G2.remove_edge("d", "b")
+ G2.add_edge("b", "d")
+
+ G1_degree = {
+ n: (in_degree, out_degree)
+ for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree)
+ }
+
+ nx.set_node_attributes(
+ G1,
+ dict(zip(G1, it.cycle(labels_many))),
+ "label",
+ )
+ nx.set_node_attributes(
+ G2,
+ dict(
+ zip(
+ [self.mapped[n] for n in G1],
+ it.cycle(labels_many),
+ )
+ ),
+ "label",
+ )
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ G2.in_degree(), G2.out_degree()
+ )
+ }
+ ),
+ )
+
+ m = {9: self.mapped[9], 1: self.mapped[1]}
+ m_rev = {self.mapped[9]: 9, self.mapped[1]: 1}
+
+ T1_out = {2, 4, 6}
+ T1_in = {5, 7, 8}
+ T1_tilde = {0, 3}
+ T2_out = {"b", "d", "f"}
+ T2_in = {"e", "g", "h"}
+ T2_tilde = {"x", "c"}
+
+ sparams = _StateParameters(
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
+ )
+
+ u = 5
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ u = 6
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ # Assign to 2, the same label as 6
+ G1.nodes[2]["label"] = G1.nodes[u]["label"]
+ G2.nodes[self.mapped[2]]["label"] = G1.nodes[u]["label"]
+ l1 = dict(G1.nodes(data="label", default=-1))
+ l2 = dict(G2.nodes(data="label", default=-1))
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ G2.in_degree(), G2.out_degree()
+ )
+ }
+ ),
+ )
+
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u], self.mapped[2]}
+
+ # Change the direction of an edge to make the degree orientation same as first candidate of u.
+ G1.remove_edge(2, 4)
+ G1.add_edge(4, 2)
+ G2.remove_edge("b", "d")
+ G2.add_edge("d", "b")
+
+ gparams = _GraphParameters(
+ G1,
+ G2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ G2.in_degree(), G2.out_degree()
+ )
+ }
+ ),
+ )
+
+ candidates = _find_candidates_Di(u, gparams, sparams, G1_degree)
+ assert candidates == {self.mapped[u]}
+
+ def test_same_in_out_degrees_no_candidate(self):
+ g1 = nx.DiGraph([(4, 1), (4, 2), (3, 4), (5, 4), (6, 4)])
+ g2 = nx.DiGraph([(1, 4), (2, 4), (3, 4), (4, 5), (4, 6)])
+
+ l1 = dict(g1.nodes(data=None, default=-1))
+ l2 = dict(g2.nodes(data=None, default=-1))
+ gparams = _GraphParameters(
+ g1,
+ g2,
+ l1,
+ l2,
+ nx.utils.groups(l1),
+ nx.utils.groups(l2),
+ nx.utils.groups(
+ {
+ node: (in_degree, out_degree)
+ for (node, in_degree), (_, out_degree) in zip(
+ g2.in_degree(), g2.out_degree()
+ )
+ }
+ ),
+ )
+
+ g1_degree = {
+ n: (in_degree, out_degree)
+ for (n, in_degree), (_, out_degree) in zip(g1.in_degree, g1.out_degree)
+ }
+
+ m = {1: 1, 2: 2, 3: 3}
+ m_rev = m.copy()
+
+ T1_out = {4}
+ T1_in = {4}
+ T1_tilde = {5, 6}
+ T2_out = {4}
+ T2_in = {4}
+ T2_tilde = {5, 6}
+
+ sparams = _StateParameters(
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
+ )
+
+ u = 4
+ # despite the same in and out degree, there's no candidate for u=4
+ candidates = _find_candidates_Di(u, gparams, sparams, g1_degree)
+ assert candidates == set()
+ # Notice how the regular candidate selection method returns wrong result.
+ assert _find_candidates(u, gparams, sparams, g1_degree) == {4}
+
+
+class TestGraphISOFeasibility:
+ def test_const_covered_neighbors(self):
+ G1 = nx.Graph([(0, 1), (1, 2), (3, 0), (3, 2)])
+ G2 = nx.Graph([("a", "b"), ("b", "c"), ("k", "a"), ("k", "c")])
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 3, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_const_no_covered_neighbors(self):
+ G1 = nx.Graph([(0, 1), (1, 2), (3, 4), (3, 5)])
+ G2 = nx.Graph([("a", "b"), ("b", "c"), ("k", "w"), ("k", "z")])
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 3, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_const_mixed_covered_uncovered_neighbors(self):
+ G1 = nx.Graph([(0, 1), (1, 2), (3, 0), (3, 2), (3, 4), (3, 5)])
+ G2 = nx.Graph(
+ [("a", "b"), ("b", "c"), ("k", "a"), ("k", "c"), ("k", "w"), ("k", "z")]
+ )
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 3, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_const_fail_cases(self):
+ G1 = nx.Graph(
+ [
+ (0, 1),
+ (1, 2),
+ (10, 0),
+ (10, 3),
+ (10, 4),
+ (10, 5),
+ (10, 6),
+ (4, 1),
+ (5, 3),
+ ]
+ )
+ G2 = nx.Graph(
+ [
+ ("a", "b"),
+ ("b", "c"),
+ ("k", "a"),
+ ("k", "d"),
+ ("k", "e"),
+ ("k", "f"),
+ ("k", "g"),
+ ("e", "b"),
+ ("f", "d"),
+ ]
+ )
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 10, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Delete one uncovered neighbor of u. Notice how it still passes the test.
+ # Two reasons for this:
+ # 1. If u, v had different degrees from the beginning, they wouldn't
+ # be selected as candidates in the first place.
+ # 2. Even if they are selected, consistency is basically 1-look-ahead,
+ # meaning that we take into consideration the relation of the
+ # candidates with their mapped neighbors. The node we deleted is
+ # not a covered neighbor.
+ # Such nodes will be checked by the cut_PT function, which is
+ # basically the 2-look-ahead, checking the relation of the
+ # candidates with T1, T2 (in which belongs the node we just deleted).
+ G1.remove_node(6)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Add one more covered neighbor of u in G1
+ G1.add_edge(u, 2)
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G2
+ G2.add_edge(v, "c")
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Add one more covered neighbor of v in G2
+ G2.add_edge(v, "x")
+ G1.add_node(7)
+ sparams.mapping.update({7: "x"})
+ sparams.reverse_mapping.update({"x": 7})
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compendate in G1
+ G1.add_edge(u, 7)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ @pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph))
+ def test_cut_inconsistent_labels(self, graph_type):
+ G1 = graph_type(
+ [
+ (0, 1),
+ (1, 2),
+ (10, 0),
+ (10, 3),
+ (10, 4),
+ (10, 5),
+ (10, 6),
+ (4, 1),
+ (5, 3),
+ ]
+ )
+ G2 = graph_type(
+ [
+ ("a", "b"),
+ ("b", "c"),
+ ("k", "a"),
+ ("k", "d"),
+ ("k", "e"),
+ ("k", "f"),
+ ("k", "g"),
+ ("e", "b"),
+ ("f", "d"),
+ ]
+ )
+
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+ l1.update({6: "green"}) # Change the label of one neighbor of u
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+
+ u, v = 10, "k"
+ assert _cut_PT(u, v, gparams, sparams)
+
+ def test_cut_consistent_labels(self):
+ G1 = nx.Graph(
+ [
+ (0, 1),
+ (1, 2),
+ (10, 0),
+ (10, 3),
+ (10, 4),
+ (10, 5),
+ (10, 6),
+ (4, 1),
+ (5, 3),
+ ]
+ )
+ G2 = nx.Graph(
+ [
+ ("a", "b"),
+ ("b", "c"),
+ ("k", "a"),
+ ("k", "d"),
+ ("k", "e"),
+ ("k", "f"),
+ ("k", "g"),
+ ("e", "b"),
+ ("f", "d"),
+ ]
+ )
+
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5},
+ None,
+ {6},
+ None,
+ {"e", "f"},
+ None,
+ {"g"},
+ None,
+ )
+
+ u, v = 10, "k"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ def test_cut_same_labels(self):
+ G1 = nx.Graph(
+ [
+ (0, 1),
+ (1, 2),
+ (10, 0),
+ (10, 3),
+ (10, 4),
+ (10, 5),
+ (10, 6),
+ (4, 1),
+ (5, 3),
+ ]
+ )
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 10: "k"}
+ G2 = nx.relabel_nodes(G1, mapped)
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5},
+ None,
+ {6},
+ None,
+ {"e", "f"},
+ None,
+ {"g"},
+ None,
+ )
+
+ u, v = 10, "k"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change intersection between G1[u] and T1, so it's not the same as the one between G2[v] and T2
+ G1.remove_edge(u, 4)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Compensate in G2
+ G2.remove_edge(v, mapped[4])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change intersection between G2[v] and T2_tilde, so it's not the same as the one between G1[u] and T1_tilde
+ G2.remove_edge(v, mapped[6])
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Compensate in G1
+ G1.remove_edge(u, 6)
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Add disconnected nodes, which will form the new Ti_out
+ G1.add_nodes_from([6, 7, 8])
+ G2.add_nodes_from(["g", "y", "z"])
+ sparams.T1_tilde.update({6, 7, 8})
+ sparams.T2_tilde.update({"g", "y", "z"})
+
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Add some new nodes to the mapping
+ sparams.mapping.update({6: "g", 7: "y"})
+ sparams.reverse_mapping.update({"g": 6, "y": 7})
+
+ # Add more nodes to T1, T2.
+ G1.add_edges_from([(6, 20), (7, 20), (6, 21)])
+ G2.add_edges_from([("g", "i"), ("g", "j"), ("y", "j")])
+
+ sparams.mapping.update({20: "j", 21: "i"})
+ sparams.reverse_mapping.update({"j": 20, "i": 21})
+ sparams.T1.update({20, 21})
+ sparams.T2.update({"i", "j"})
+ sparams.T1_tilde.difference_update({6, 7})
+ sparams.T2_tilde.difference_update({"g", "y"})
+
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Add nodes from the new T1 and T2, as neighbors of u and v respectively
+ G1.add_edges_from([(u, 20), (u, 21)])
+ G2.add_edges_from([(v, "i"), (v, "j")])
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the edges, maintaining the G1[u]-T1 intersection
+ G1.remove_edge(u, 20)
+ G1.add_edge(u, 4)
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Connect u to 8 which is still in T1_tilde
+ G1.add_edge(u, 8)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same for v and z, so that inters(G1[u], T1out) == inters(G2[v], T2out)
+ G2.add_edge(v, "z")
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ def test_cut_different_labels(self):
+ G1 = nx.Graph(
+ [
+ (0, 1),
+ (1, 2),
+ (1, 14),
+ (0, 4),
+ (1, 5),
+ (2, 6),
+ (3, 7),
+ (3, 6),
+ (4, 10),
+ (4, 9),
+ (6, 10),
+ (20, 9),
+ (20, 15),
+ (20, 12),
+ (20, 11),
+ (12, 13),
+ (11, 13),
+ (20, 8),
+ (20, 3),
+ (20, 5),
+ (20, 0),
+ ]
+ )
+ mapped = {
+ 0: "a",
+ 1: "b",
+ 2: "c",
+ 3: "d",
+ 4: "e",
+ 5: "f",
+ 6: "g",
+ 7: "h",
+ 8: "i",
+ 9: "j",
+ 10: "k",
+ 11: "l",
+ 12: "m",
+ 13: "n",
+ 14: "o",
+ 15: "p",
+ 20: "x",
+ }
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ l1 = {n: "none" for n in G1.nodes()}
+ l2 = {}
+
+ l1.update(
+ {
+ 9: "blue",
+ 15: "blue",
+ 12: "blue",
+ 11: "green",
+ 3: "green",
+ 8: "red",
+ 0: "red",
+ 5: "yellow",
+ }
+ )
+ l2.update({mapped[n]: l for n, l in l1.items()})
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5, 6, 7, 14},
+ None,
+ {9, 10, 15, 12, 11, 13, 8},
+ None,
+ {"e", "f", "g", "h", "o"},
+ None,
+ {"j", "k", "l", "m", "n", "i", "p"},
+ None,
+ )
+
+ u, v = 20, "x"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the orientation of the labels on neighbors of u compared to neighbors of v. Leave the structure intact
+ l1.update({9: "red"})
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # compensate in G2
+ l2.update({mapped[9]: "red"})
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the intersection of G1[u] and T1
+ G1.add_edge(u, 4)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same for G2[v] and T2
+ G2.add_edge(v, mapped[4])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the intersection of G2[v] and T2_tilde
+ G2.remove_edge(v, mapped[8])
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same for G1[u] and T1_tilde
+ G1.remove_edge(u, 8)
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Place 8 and mapped[8] in T1 and T2 respectively, by connecting it to covered nodes
+ G1.add_edge(8, 3)
+ G2.add_edge(mapped[8], mapped[3])
+ sparams.T1.add(8)
+ sparams.T2.add(mapped[8])
+ sparams.T1_tilde.remove(8)
+ sparams.T2_tilde.remove(mapped[8])
+
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Remove neighbor of u from T1
+ G1.remove_node(5)
+ l1.pop(5)
+ sparams.T1.remove(5)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same in G2
+ G2.remove_node(mapped[5])
+ l2.pop(mapped[5])
+ sparams.T2.remove(mapped[5])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ def test_feasibility_same_labels(self):
+ G1 = nx.Graph(
+ [
+ (0, 1),
+ (1, 2),
+ (1, 14),
+ (0, 4),
+ (1, 5),
+ (2, 6),
+ (3, 7),
+ (3, 6),
+ (4, 10),
+ (4, 9),
+ (6, 10),
+ (20, 9),
+ (20, 15),
+ (20, 12),
+ (20, 11),
+ (12, 13),
+ (11, 13),
+ (20, 8),
+ (20, 2),
+ (20, 5),
+ (20, 0),
+ ]
+ )
+ mapped = {
+ 0: "a",
+ 1: "b",
+ 2: "c",
+ 3: "d",
+ 4: "e",
+ 5: "f",
+ 6: "g",
+ 7: "h",
+ 8: "i",
+ 9: "j",
+ 10: "k",
+ 11: "l",
+ 12: "m",
+ 13: "n",
+ 14: "o",
+ 15: "p",
+ 20: "x",
+ }
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {mapped[n]: "blue" for n in G1.nodes()}
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5, 6, 7, 14},
+ None,
+ {9, 10, 15, 12, 11, 13, 8},
+ None,
+ {"e", "f", "g", "h", "o"},
+ None,
+ {"j", "k", "l", "m", "n", "i", "p"},
+ None,
+ )
+
+ u, v = 20, "x"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change structure in G2 such that, ONLY consistency is harmed
+ G2.remove_edge(mapped[20], mapped[2])
+ G2.add_edge(mapped[20], mapped[3])
+
+ # Consistency check fails, while the cutting rules are satisfied!
+ assert not _cut_PT(u, v, gparams, sparams)
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G1 and make it consistent
+ G1.remove_edge(20, 2)
+ G1.add_edge(20, 3)
+ assert not _cut_PT(u, v, gparams, sparams)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # ONLY fail the cutting check
+ G2.add_edge(v, mapped[10])
+ assert _cut_PT(u, v, gparams, sparams)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_feasibility_different_labels(self):
+ G1 = nx.Graph(
+ [
+ (0, 1),
+ (1, 2),
+ (1, 14),
+ (0, 4),
+ (1, 5),
+ (2, 6),
+ (3, 7),
+ (3, 6),
+ (4, 10),
+ (4, 9),
+ (6, 10),
+ (20, 9),
+ (20, 15),
+ (20, 12),
+ (20, 11),
+ (12, 13),
+ (11, 13),
+ (20, 8),
+ (20, 2),
+ (20, 5),
+ (20, 0),
+ ]
+ )
+ mapped = {
+ 0: "a",
+ 1: "b",
+ 2: "c",
+ 3: "d",
+ 4: "e",
+ 5: "f",
+ 6: "g",
+ 7: "h",
+ 8: "i",
+ 9: "j",
+ 10: "k",
+ 11: "l",
+ 12: "m",
+ 13: "n",
+ 14: "o",
+ 15: "p",
+ 20: "x",
+ }
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ l1 = {n: "none" for n in G1.nodes()}
+ l2 = {}
+
+ l1.update(
+ {
+ 9: "blue",
+ 15: "blue",
+ 12: "blue",
+ 11: "green",
+ 2: "green",
+ 8: "red",
+ 0: "red",
+ 5: "yellow",
+ }
+ )
+ l2.update({mapped[n]: l for n, l in l1.items()})
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5, 6, 7, 14},
+ None,
+ {9, 10, 15, 12, 11, 13, 8},
+ None,
+ {"e", "f", "g", "h", "o"},
+ None,
+ {"j", "k", "l", "m", "n", "i", "p"},
+ None,
+ )
+
+ u, v = 20, "x"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change structure in G2 such that, ONLY consistency is harmed
+ G2.remove_edge(mapped[20], mapped[2])
+ G2.add_edge(mapped[20], mapped[3])
+ l2.update({mapped[3]: "green"})
+
+ # Consistency check fails, while the cutting rules are satisfied!
+ assert not _cut_PT(u, v, gparams, sparams)
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G1 and make it consistent
+ G1.remove_edge(20, 2)
+ G1.add_edge(20, 3)
+ l1.update({3: "green"})
+ assert not _cut_PT(u, v, gparams, sparams)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # ONLY fail the cutting check
+ l1.update({5: "red"})
+ assert _cut_PT(u, v, gparams, sparams)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+
+class TestMultiGraphISOFeasibility:
+ def test_const_covered_neighbors(self):
+ G1 = nx.MultiGraph(
+ [(0, 1), (0, 1), (1, 2), (3, 0), (3, 0), (3, 0), (3, 2), (3, 2)]
+ )
+ G2 = nx.MultiGraph(
+ [
+ ("a", "b"),
+ ("a", "b"),
+ ("b", "c"),
+ ("k", "a"),
+ ("k", "a"),
+ ("k", "a"),
+ ("k", "c"),
+ ("k", "c"),
+ ]
+ )
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 3, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_const_no_covered_neighbors(self):
+ G1 = nx.MultiGraph([(0, 1), (0, 1), (1, 2), (3, 4), (3, 4), (3, 5)])
+ G2 = nx.MultiGraph([("a", "b"), ("b", "c"), ("k", "w"), ("k", "w"), ("k", "z")])
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 3, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_const_mixed_covered_uncovered_neighbors(self):
+ G1 = nx.MultiGraph(
+ [(0, 1), (1, 2), (3, 0), (3, 0), (3, 0), (3, 2), (3, 2), (3, 4), (3, 5)]
+ )
+ G2 = nx.MultiGraph(
+ [
+ ("a", "b"),
+ ("b", "c"),
+ ("k", "a"),
+ ("k", "a"),
+ ("k", "a"),
+ ("k", "c"),
+ ("k", "c"),
+ ("k", "w"),
+ ("k", "z"),
+ ]
+ )
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 3, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_const_fail_cases(self):
+ G1 = nx.MultiGraph(
+ [
+ (0, 1),
+ (1, 2),
+ (10, 0),
+ (10, 0),
+ (10, 0),
+ (10, 3),
+ (10, 3),
+ (10, 4),
+ (10, 5),
+ (10, 6),
+ (10, 6),
+ (4, 1),
+ (5, 3),
+ ]
+ )
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 10: "k"}
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 10, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Delete one uncovered neighbor of u. Notice how it still passes the test. Two reasons for this:
+ # 1. If u, v had different degrees from the beginning, they wouldn't be selected as candidates in the first
+ # place.
+ # 2. Even if they are selected, consistency is basically 1-look-ahead, meaning that we take into consideration
+ # the relation of the candidates with their mapped neighbors. The node we deleted is not a covered neighbor.
+ # Such nodes will be checked by the cut_PT function, which is basically the 2-look-ahead, checking the
+ # relation of the candidates with T1, T2 (in which belongs the node we just deleted).
+ G1.remove_node(6)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Add one more covered neighbor of u in G1
+ G1.add_edge(u, 2)
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G2
+ G2.add_edge(v, "c")
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Add one more covered neighbor of v in G2
+ G2.add_edge(v, "x")
+ G1.add_node(7)
+ sparams.mapping.update({7: "x"})
+ sparams.reverse_mapping.update({"x": 7})
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compendate in G1
+ G1.add_edge(u, 7)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Delete an edge between u and a covered neighbor
+ G1.remove_edges_from([(u, 0), (u, 0)])
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G2
+ G2.remove_edges_from([(v, mapped[0]), (v, mapped[0])])
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Remove an edge between v and a covered neighbor
+ G2.remove_edge(v, mapped[3])
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G1
+ G1.remove_edge(u, 3)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_cut_same_labels(self):
+ G1 = nx.MultiGraph(
+ [
+ (0, 1),
+ (1, 2),
+ (10, 0),
+ (10, 0),
+ (10, 0),
+ (10, 3),
+ (10, 3),
+ (10, 4),
+ (10, 4),
+ (10, 5),
+ (10, 5),
+ (10, 5),
+ (10, 5),
+ (10, 6),
+ (10, 6),
+ (4, 1),
+ (5, 3),
+ ]
+ )
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 10: "k"}
+ G2 = nx.relabel_nodes(G1, mapped)
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5},
+ None,
+ {6},
+ None,
+ {"e", "f"},
+ None,
+ {"g"},
+ None,
+ )
+
+ u, v = 10, "k"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Remove one of the multiple edges between u and a neighbor
+ G1.remove_edge(u, 4)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Compensate in G2
+ G1.remove_edge(u, 4)
+ G2.remove_edges_from([(v, mapped[4]), (v, mapped[4])])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change intersection between G2[v] and T2_tilde, so it's not the same as the one between G1[u] and T1_tilde
+ G2.remove_edge(v, mapped[6])
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Compensate in G1
+ G1.remove_edge(u, 6)
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Add more edges between u and neighbor which belongs in T1_tilde
+ G1.add_edges_from([(u, 5), (u, 5), (u, 5)])
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Compensate in G2
+ G2.add_edges_from([(v, mapped[5]), (v, mapped[5]), (v, mapped[5])])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Add disconnected nodes, which will form the new Ti_out
+ G1.add_nodes_from([6, 7, 8])
+ G2.add_nodes_from(["g", "y", "z"])
+ G1.add_edges_from([(u, 6), (u, 6), (u, 6), (u, 8)])
+ G2.add_edges_from([(v, "g"), (v, "g"), (v, "g"), (v, "z")])
+
+ sparams.T1_tilde.update({6, 7, 8})
+ sparams.T2_tilde.update({"g", "y", "z"})
+
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Add some new nodes to the mapping
+ sparams.mapping.update({6: "g", 7: "y"})
+ sparams.reverse_mapping.update({"g": 6, "y": 7})
+
+ # Add more nodes to T1, T2.
+ G1.add_edges_from([(6, 20), (7, 20), (6, 21)])
+ G2.add_edges_from([("g", "i"), ("g", "j"), ("y", "j")])
+
+ sparams.T1.update({20, 21})
+ sparams.T2.update({"i", "j"})
+ sparams.T1_tilde.difference_update({6, 7})
+ sparams.T2_tilde.difference_update({"g", "y"})
+
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Remove some edges
+ G2.remove_edge(v, "g")
+ assert _cut_PT(u, v, gparams, sparams)
+
+ G1.remove_edge(u, 6)
+ G1.add_edge(u, 8)
+ G2.add_edge(v, "z")
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Add nodes from the new T1 and T2, as neighbors of u and v respectively
+ G1.add_edges_from([(u, 20), (u, 20), (u, 20), (u, 21)])
+ G2.add_edges_from([(v, "i"), (v, "i"), (v, "i"), (v, "j")])
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the edges
+ G1.remove_edge(u, 20)
+ G1.add_edge(u, 4)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ G2.remove_edge(v, "i")
+ G2.add_edge(v, mapped[4])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ def test_cut_different_labels(self):
+ G1 = nx.MultiGraph(
+ [
+ (0, 1),
+ (0, 1),
+ (1, 2),
+ (1, 2),
+ (1, 14),
+ (0, 4),
+ (1, 5),
+ (2, 6),
+ (3, 7),
+ (3, 6),
+ (4, 10),
+ (4, 9),
+ (6, 10),
+ (20, 9),
+ (20, 9),
+ (20, 9),
+ (20, 15),
+ (20, 15),
+ (20, 12),
+ (20, 11),
+ (20, 11),
+ (20, 11),
+ (12, 13),
+ (11, 13),
+ (20, 8),
+ (20, 8),
+ (20, 3),
+ (20, 3),
+ (20, 5),
+ (20, 5),
+ (20, 5),
+ (20, 0),
+ (20, 0),
+ (20, 0),
+ ]
+ )
+ mapped = {
+ 0: "a",
+ 1: "b",
+ 2: "c",
+ 3: "d",
+ 4: "e",
+ 5: "f",
+ 6: "g",
+ 7: "h",
+ 8: "i",
+ 9: "j",
+ 10: "k",
+ 11: "l",
+ 12: "m",
+ 13: "n",
+ 14: "o",
+ 15: "p",
+ 20: "x",
+ }
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ l1 = {n: "none" for n in G1.nodes()}
+ l2 = {}
+
+ l1.update(
+ {
+ 9: "blue",
+ 15: "blue",
+ 12: "blue",
+ 11: "green",
+ 3: "green",
+ 8: "red",
+ 0: "red",
+ 5: "yellow",
+ }
+ )
+ l2.update({mapped[n]: l for n, l in l1.items()})
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5, 6, 7, 14},
+ None,
+ {9, 10, 15, 12, 11, 13, 8},
+ None,
+ {"e", "f", "g", "h", "o"},
+ None,
+ {"j", "k", "l", "m", "n", "i", "p"},
+ None,
+ )
+
+ u, v = 20, "x"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the orientation of the labels on neighbors of u compared to neighbors of v. Leave the structure intact
+ l1.update({9: "red"})
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # compensate in G2
+ l2.update({mapped[9]: "red"})
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the intersection of G1[u] and T1
+ G1.add_edge(u, 4)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same for G2[v] and T2
+ G2.add_edge(v, mapped[4])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Delete one from the multiple edges
+ G2.remove_edge(v, mapped[8])
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same for G1[u] and T1_tilde
+ G1.remove_edge(u, 8)
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Place 8 and mapped[8] in T1 and T2 respectively, by connecting it to covered nodes
+ G1.add_edges_from([(8, 3), (8, 3), (8, u)])
+ G2.add_edges_from([(mapped[8], mapped[3]), (mapped[8], mapped[3])])
+ sparams.T1.add(8)
+ sparams.T2.add(mapped[8])
+ sparams.T1_tilde.remove(8)
+ sparams.T2_tilde.remove(mapped[8])
+
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Fix uneven edges
+ G1.remove_edge(8, u)
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Remove neighbor of u from T1
+ G1.remove_node(5)
+ l1.pop(5)
+ sparams.T1.remove(5)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same in G2
+ G2.remove_node(mapped[5])
+ l2.pop(mapped[5])
+ sparams.T2.remove(mapped[5])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ def test_feasibility_same_labels(self):
+ G1 = nx.MultiGraph(
+ [
+ (0, 1),
+ (0, 1),
+ (1, 2),
+ (1, 2),
+ (1, 14),
+ (0, 4),
+ (1, 5),
+ (2, 6),
+ (3, 7),
+ (3, 6),
+ (4, 10),
+ (4, 9),
+ (6, 10),
+ (20, 9),
+ (20, 9),
+ (20, 9),
+ (20, 15),
+ (20, 15),
+ (20, 12),
+ (20, 11),
+ (20, 11),
+ (20, 11),
+ (12, 13),
+ (11, 13),
+ (20, 8),
+ (20, 8),
+ (20, 3),
+ (20, 3),
+ (20, 5),
+ (20, 5),
+ (20, 5),
+ (20, 0),
+ (20, 0),
+ (20, 0),
+ ]
+ )
+ mapped = {
+ 0: "a",
+ 1: "b",
+ 2: "c",
+ 3: "d",
+ 4: "e",
+ 5: "f",
+ 6: "g",
+ 7: "h",
+ 8: "i",
+ 9: "j",
+ 10: "k",
+ 11: "l",
+ 12: "m",
+ 13: "n",
+ 14: "o",
+ 15: "p",
+ 20: "x",
+ }
+ G2 = nx.relabel_nodes(G1, mapped)
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {mapped[n]: "blue" for n in G1.nodes()}
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5, 6, 7, 14},
+ None,
+ {9, 10, 15, 12, 11, 13, 8},
+ None,
+ {"e", "f", "g", "h", "o"},
+ None,
+ {"j", "k", "l", "m", "n", "i", "p"},
+ None,
+ )
+
+ u, v = 20, "x"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change structure in G2 such that, ONLY consistency is harmed
+ G2.remove_edges_from([(mapped[20], mapped[3]), (mapped[20], mapped[3])])
+ G2.add_edges_from([(mapped[20], mapped[2]), (mapped[20], mapped[2])])
+
+ # Consistency check fails, while the cutting rules are satisfied!
+ assert not _cut_PT(u, v, gparams, sparams)
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G1 and make it consistent
+ G1.remove_edges_from([(20, 3), (20, 3)])
+ G1.add_edges_from([(20, 2), (20, 2)])
+ assert not _cut_PT(u, v, gparams, sparams)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # ONLY fail the cutting check
+ G2.add_edges_from([(v, mapped[10])] * 5)
+ assert _cut_PT(u, v, gparams, sparams)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Pass all tests
+ G1.add_edges_from([(u, 10)] * 5)
+ assert not _cut_PT(u, v, gparams, sparams)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_feasibility_different_labels(self):
+ G1 = nx.MultiGraph(
+ [
+ (0, 1),
+ (0, 1),
+ (1, 2),
+ (1, 2),
+ (1, 14),
+ (0, 4),
+ (1, 5),
+ (2, 6),
+ (3, 7),
+ (3, 6),
+ (4, 10),
+ (4, 9),
+ (6, 10),
+ (20, 9),
+ (20, 9),
+ (20, 9),
+ (20, 15),
+ (20, 15),
+ (20, 12),
+ (20, 11),
+ (20, 11),
+ (20, 11),
+ (12, 13),
+ (11, 13),
+ (20, 8),
+ (20, 8),
+ (20, 2),
+ (20, 2),
+ (20, 5),
+ (20, 5),
+ (20, 5),
+ (20, 0),
+ (20, 0),
+ (20, 0),
+ ]
+ )
+ mapped = {
+ 0: "a",
+ 1: "b",
+ 2: "c",
+ 3: "d",
+ 4: "e",
+ 5: "f",
+ 6: "g",
+ 7: "h",
+ 8: "i",
+ 9: "j",
+ 10: "k",
+ 11: "l",
+ 12: "m",
+ 13: "n",
+ 14: "o",
+ 15: "p",
+ 20: "x",
+ }
+ G2 = nx.relabel_nodes(G1, mapped)
+ l1 = {n: "none" for n in G1.nodes()}
+ l2 = {}
+
+ l1.update(
+ {
+ 9: "blue",
+ 15: "blue",
+ 12: "blue",
+ 11: "green",
+ 2: "green",
+ 8: "red",
+ 0: "red",
+ 5: "yellow",
+ }
+ )
+ l2.update({mapped[n]: l for n, l in l1.items()})
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5, 6, 7, 14},
+ None,
+ {9, 10, 15, 12, 11, 13, 8},
+ None,
+ {"e", "f", "g", "h", "o"},
+ None,
+ {"j", "k", "l", "m", "n", "i", "p"},
+ None,
+ )
+
+ u, v = 20, "x"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change structure in G2 such that, ONLY consistency is harmed
+ G2.remove_edges_from([(mapped[20], mapped[2]), (mapped[20], mapped[2])])
+ G2.add_edges_from([(mapped[20], mapped[3]), (mapped[20], mapped[3])])
+ l2.update({mapped[3]: "green"})
+
+ # Consistency check fails, while the cutting rules are satisfied!
+ assert not _cut_PT(u, v, gparams, sparams)
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G1 and make it consistent
+ G1.remove_edges_from([(20, 2), (20, 2)])
+ G1.add_edges_from([(20, 3), (20, 3)])
+ l1.update({3: "green"})
+ assert not _cut_PT(u, v, gparams, sparams)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # ONLY fail the cutting check
+ l1.update({5: "red"})
+ assert _cut_PT(u, v, gparams, sparams)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+
+class TestDiGraphISOFeasibility:
+ def test_const_covered_neighbors(self):
+ G1 = nx.DiGraph([(0, 1), (1, 2), (0, 3), (2, 3)])
+ G2 = nx.DiGraph([("a", "b"), ("b", "c"), ("a", "k"), ("c", "k")])
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 3, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_const_no_covered_neighbors(self):
+ G1 = nx.DiGraph([(0, 1), (1, 2), (3, 4), (3, 5)])
+ G2 = nx.DiGraph([("a", "b"), ("b", "c"), ("k", "w"), ("k", "z")])
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 3, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_const_mixed_covered_uncovered_neighbors(self):
+ G1 = nx.DiGraph([(0, 1), (1, 2), (3, 0), (3, 2), (3, 4), (3, 5)])
+ G2 = nx.DiGraph(
+ [("a", "b"), ("b", "c"), ("k", "a"), ("k", "c"), ("k", "w"), ("k", "z")]
+ )
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 3, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_const_fail_cases(self):
+ G1 = nx.DiGraph(
+ [
+ (0, 1),
+ (2, 1),
+ (10, 0),
+ (10, 3),
+ (10, 4),
+ (5, 10),
+ (10, 6),
+ (1, 4),
+ (5, 3),
+ ]
+ )
+ G2 = nx.DiGraph(
+ [
+ ("a", "b"),
+ ("c", "b"),
+ ("k", "a"),
+ ("k", "d"),
+ ("k", "e"),
+ ("f", "k"),
+ ("k", "g"),
+ ("b", "e"),
+ ("f", "d"),
+ ]
+ )
+ gparams = _GraphParameters(G1, G2, None, None, None, None, None)
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ u, v = 10, "k"
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Delete one uncovered neighbor of u. Notice how it still passes the
+ # test. Two reasons for this:
+ # 1. If u, v had different degrees from the beginning, they wouldn't
+ # be selected as candidates in the first place.
+ # 2. Even if they are selected, consistency is basically
+ # 1-look-ahead, meaning that we take into consideration the
+ # relation of the candidates with their mapped neighbors.
+ # The node we deleted is not a covered neighbor.
+ # Such nodes will be checked by the cut_PT function, which is
+ # basically the 2-look-ahead, checking the relation of the
+ # candidates with T1, T2 (in which belongs the node we just deleted).
+ G1.remove_node(6)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Add one more covered neighbor of u in G1
+ G1.add_edge(u, 2)
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G2
+ G2.add_edge(v, "c")
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ # Add one more covered neighbor of v in G2
+ G2.add_edge(v, "x")
+ G1.add_node(7)
+ sparams.mapping.update({7: "x"})
+ sparams.reverse_mapping.update({"x": 7})
+ assert not _consistent_PT(u, v, gparams, sparams)
+
+ # Compensate in G1
+ G1.add_edge(u, 7)
+ assert _consistent_PT(u, v, gparams, sparams)
+
+ def test_cut_inconsistent_labels(self):
+ G1 = nx.DiGraph(
+ [
+ (0, 1),
+ (2, 1),
+ (10, 0),
+ (10, 3),
+ (10, 4),
+ (5, 10),
+ (10, 6),
+ (1, 4),
+ (5, 3),
+ ]
+ )
+ G2 = nx.DiGraph(
+ [
+ ("a", "b"),
+ ("c", "b"),
+ ("k", "a"),
+ ("k", "d"),
+ ("k", "e"),
+ ("f", "k"),
+ ("k", "g"),
+ ("b", "e"),
+ ("f", "d"),
+ ]
+ )
+
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+ l1.update({5: "green"}) # Change the label of one neighbor of u
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+
+ u, v = 10, "k"
+ assert _cut_PT(u, v, gparams, sparams)
+
+ def test_cut_consistent_labels(self):
+ G1 = nx.DiGraph(
+ [
+ (0, 1),
+ (2, 1),
+ (10, 0),
+ (10, 3),
+ (10, 4),
+ (5, 10),
+ (10, 6),
+ (1, 4),
+ (5, 3),
+ ]
+ )
+ G2 = nx.DiGraph(
+ [
+ ("a", "b"),
+ ("c", "b"),
+ ("k", "a"),
+ ("k", "d"),
+ ("k", "e"),
+ ("f", "k"),
+ ("k", "g"),
+ ("b", "e"),
+ ("f", "d"),
+ ]
+ )
+
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4},
+ {5, 10},
+ {6},
+ None,
+ {"e"},
+ {"f", "k"},
+ {"g"},
+ None,
+ )
+
+ u, v = 10, "k"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ def test_cut_same_labels(self):
+ G1 = nx.DiGraph(
+ [
+ (0, 1),
+ (2, 1),
+ (10, 0),
+ (10, 3),
+ (10, 4),
+ (5, 10),
+ (10, 6),
+ (1, 4),
+ (5, 3),
+ ]
+ )
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 10: "k"}
+ G2 = nx.relabel_nodes(G1, mapped)
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4},
+ {5, 10},
+ {6},
+ None,
+ {"e"},
+ {"f", "k"},
+ {"g"},
+ None,
+ )
+
+ u, v = 10, "k"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change intersection between G1[u] and T1_out, so it's not the same as the one between G2[v] and T2_out
+ G1.remove_edge(u, 4)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Compensate in G2
+ G2.remove_edge(v, mapped[4])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change intersection between G1[u] and T1_in, so it's not the same as the one between G2[v] and T2_in
+ G1.remove_edge(5, u)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Compensate in G2
+ G2.remove_edge(mapped[5], v)
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change intersection between G2[v] and T2_tilde, so it's not the same as the one between G1[u] and T1_tilde
+ G2.remove_edge(v, mapped[6])
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Compensate in G1
+ G1.remove_edge(u, 6)
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Add disconnected nodes, which will form the new Ti_tilde
+ G1.add_nodes_from([6, 7, 8])
+ G2.add_nodes_from(["g", "y", "z"])
+ sparams.T1_tilde.update({6, 7, 8})
+ sparams.T2_tilde.update({"g", "y", "z"})
+
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ def test_cut_different_labels(self):
+ G1 = nx.DiGraph(
+ [
+ (0, 1),
+ (1, 2),
+ (14, 1),
+ (0, 4),
+ (1, 5),
+ (2, 6),
+ (3, 7),
+ (3, 6),
+ (10, 4),
+ (4, 9),
+ (6, 10),
+ (20, 9),
+ (20, 15),
+ (20, 12),
+ (20, 11),
+ (12, 13),
+ (11, 13),
+ (20, 8),
+ (20, 3),
+ (20, 5),
+ (0, 20),
+ ]
+ )
+ mapped = {
+ 0: "a",
+ 1: "b",
+ 2: "c",
+ 3: "d",
+ 4: "e",
+ 5: "f",
+ 6: "g",
+ 7: "h",
+ 8: "i",
+ 9: "j",
+ 10: "k",
+ 11: "l",
+ 12: "m",
+ 13: "n",
+ 14: "o",
+ 15: "p",
+ 20: "x",
+ }
+ G2 = nx.relabel_nodes(G1, mapped)
+
+ l1 = {n: "none" for n in G1.nodes()}
+ l2 = {}
+
+ l1.update(
+ {
+ 9: "blue",
+ 15: "blue",
+ 12: "blue",
+ 11: "green",
+ 3: "green",
+ 8: "red",
+ 0: "red",
+ 5: "yellow",
+ }
+ )
+ l2.update({mapped[n]: l for n, l in l1.items()})
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c", 3: "d"},
+ {"a": 0, "b": 1, "c": 2, "d": 3},
+ {4, 5, 6, 7, 20},
+ {14, 20},
+ {9, 10, 15, 12, 11, 13, 8},
+ None,
+ {"e", "f", "g", "x"},
+ {"o", "x"},
+ {"j", "k", "l", "m", "n", "i", "p"},
+ None,
+ )
+
+ u, v = 20, "x"
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the orientation of the labels on neighbors of u compared to neighbors of v. Leave the structure intact
+ l1.update({9: "red"})
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # compensate in G2
+ l2.update({mapped[9]: "red"})
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the intersection of G1[u] and T1_out
+ G1.add_edge(u, 4)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same for G2[v] and T2_out
+ G2.add_edge(v, mapped[4])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the intersection of G1[u] and T1_in
+ G1.add_edge(u, 14)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same for G2[v] and T2_in
+ G2.add_edge(v, mapped[14])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Change the intersection of G2[v] and T2_tilde
+ G2.remove_edge(v, mapped[8])
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same for G1[u] and T1_tilde
+ G1.remove_edge(u, 8)
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Place 8 and mapped[8] in T1 and T2 respectively, by connecting it to covered nodes
+ G1.add_edge(8, 3)
+ G2.add_edge(mapped[8], mapped[3])
+ sparams.T1.add(8)
+ sparams.T2.add(mapped[8])
+ sparams.T1_tilde.remove(8)
+ sparams.T2_tilde.remove(mapped[8])
+
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ # Remove neighbor of u from T1
+ G1.remove_node(5)
+ l1.pop(5)
+ sparams.T1.remove(5)
+ assert _cut_PT(u, v, gparams, sparams)
+
+ # Same in G2
+ G2.remove_node(mapped[5])
+ l2.pop(mapped[5])
+ sparams.T2.remove(mapped[5])
+ assert not _cut_PT(u, v, gparams, sparams)
+
+ def test_predecessor_T1_in_fail(self):
+ G1 = nx.DiGraph(
+ [(0, 1), (0, 3), (4, 0), (1, 5), (5, 2), (3, 6), (4, 6), (6, 5)]
+ )
+ mapped = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g"}
+ G2 = nx.relabel_nodes(G1, mapped)
+ l1 = {n: "blue" for n in G1.nodes()}
+ l2 = {n: "blue" for n in G2.nodes()}
+
+ gparams = _GraphParameters(
+ G1, G2, l1, l2, nx.utils.groups(l1), nx.utils.groups(l2), None
+ )
+ sparams = _StateParameters(
+ {0: "a", 1: "b", 2: "c"},
+ {"a": 0, "b": 1, "c": 2},
+ {3, 5},
+ {4, 5},
+ {6},
+ None,
+ {"d", "f"},
+ {"f"}, # mapped[4] is missing from T2_in
+ {"g"},
+ None,
+ )
+
+ u, v = 6, "g"
+ assert _cut_PT(u, v, gparams, sparams)
+
+ sparams.T2_in.add("e")
+ assert not _cut_PT(u, v, gparams, sparams)
+
+
+class TestGraphTinoutUpdating:
+ edges = [
+ (1, 3),
+ (2, 3),
+ (3, 4),
+ (4, 9),
+ (4, 5),
+ (3, 9),
+ (5, 8),
+ (5, 7),
+ (8, 7),
+ (6, 7),
+ ]
+ mapped = {
+ 0: "x",
+ 1: "a",
+ 2: "b",
+ 3: "c",
+ 4: "d",
+ 5: "e",
+ 6: "f",
+ 7: "g",
+ 8: "h",
+ 9: "i",
+ }
+ G1 = nx.Graph()
+ G1.add_edges_from(edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, mapping=mapped)
+
+ def test_updating(self):
+ G2_degree = dict(self.G2.degree)
+ gparams, sparams = _initialize_parameters(self.G1, self.G2, G2_degree)
+ m, m_rev, T1, _, T1_tilde, _, T2, _, T2_tilde, _ = sparams
+
+ # Add node to the mapping
+ m[4] = self.mapped[4]
+ m_rev[self.mapped[4]] = 4
+ _update_Tinout(4, self.mapped[4], gparams, sparams)
+
+ assert T1 == {3, 5, 9}
+ assert T2 == {"c", "i", "e"}
+ assert T1_tilde == {0, 1, 2, 6, 7, 8}
+ assert T2_tilde == {"x", "a", "b", "f", "g", "h"}
+
+ # Add node to the mapping
+ m[5] = self.mapped[5]
+ m_rev.update({self.mapped[5]: 5})
+ _update_Tinout(5, self.mapped[5], gparams, sparams)
+
+ assert T1 == {3, 9, 8, 7}
+ assert T2 == {"c", "i", "h", "g"}
+ assert T1_tilde == {0, 1, 2, 6}
+ assert T2_tilde == {"x", "a", "b", "f"}
+
+ # Add node to the mapping
+ m[6] = self.mapped[6]
+ m_rev.update({self.mapped[6]: 6})
+ _update_Tinout(6, self.mapped[6], gparams, sparams)
+
+ assert T1 == {3, 9, 8, 7}
+ assert T2 == {"c", "i", "h", "g"}
+ assert T1_tilde == {0, 1, 2}
+ assert T2_tilde == {"x", "a", "b"}
+
+ # Add node to the mapping
+ m[3] = self.mapped[3]
+ m_rev.update({self.mapped[3]: 3})
+ _update_Tinout(3, self.mapped[3], gparams, sparams)
+
+ assert T1 == {1, 2, 9, 8, 7}
+ assert T2 == {"a", "b", "i", "h", "g"}
+ assert T1_tilde == {0}
+ assert T2_tilde == {"x"}
+
+ # Add node to the mapping
+ m[0] = self.mapped[0]
+ m_rev.update({self.mapped[0]: 0})
+ _update_Tinout(0, self.mapped[0], gparams, sparams)
+
+ assert T1 == {1, 2, 9, 8, 7}
+ assert T2 == {"a", "b", "i", "h", "g"}
+ assert T1_tilde == set()
+ assert T2_tilde == set()
+
+ def test_restoring(self):
+ m = {0: "x", 3: "c", 4: "d", 5: "e", 6: "f"}
+ m_rev = {"x": 0, "c": 3, "d": 4, "e": 5, "f": 6}
+
+ T1 = {1, 2, 7, 9, 8}
+ T2 = {"a", "b", "g", "i", "h"}
+ T1_tilde = set()
+ T2_tilde = set()
+
+ gparams = _GraphParameters(self.G1, self.G2, {}, {}, {}, {}, {})
+ sparams = _StateParameters(
+ m, m_rev, T1, None, T1_tilde, None, T2, None, T2_tilde, None
+ )
+
+ # Remove a node from the mapping
+ m.pop(0)
+ m_rev.pop("x")
+ _restore_Tinout(0, self.mapped[0], gparams, sparams)
+
+ assert T1 == {1, 2, 7, 9, 8}
+ assert T2 == {"a", "b", "g", "i", "h"}
+ assert T1_tilde == {0}
+ assert T2_tilde == {"x"}
+
+ # Remove a node from the mapping
+ m.pop(6)
+ m_rev.pop("f")
+ _restore_Tinout(6, self.mapped[6], gparams, sparams)
+
+ assert T1 == {1, 2, 7, 9, 8}
+ assert T2 == {"a", "b", "g", "i", "h"}
+ assert T1_tilde == {0, 6}
+ assert T2_tilde == {"x", "f"}
+
+ # Remove a node from the mapping
+ m.pop(3)
+ m_rev.pop("c")
+ _restore_Tinout(3, self.mapped[3], gparams, sparams)
+
+ assert T1 == {7, 9, 8, 3}
+ assert T2 == {"g", "i", "h", "c"}
+ assert T1_tilde == {0, 6, 1, 2}
+ assert T2_tilde == {"x", "f", "a", "b"}
+
+ # Remove a node from the mapping
+ m.pop(5)
+ m_rev.pop("e")
+ _restore_Tinout(5, self.mapped[5], gparams, sparams)
+
+ assert T1 == {9, 3, 5}
+ assert T2 == {"i", "c", "e"}
+ assert T1_tilde == {0, 6, 1, 2, 7, 8}
+ assert T2_tilde == {"x", "f", "a", "b", "g", "h"}
+
+ # Remove a node from the mapping
+ m.pop(4)
+ m_rev.pop("d")
+ _restore_Tinout(4, self.mapped[4], gparams, sparams)
+
+ assert T1 == set()
+ assert T2 == set()
+ assert T1_tilde == set(self.G1.nodes())
+ assert T2_tilde == set(self.G2.nodes())
+
+
+class TestDiGraphTinoutUpdating:
+ edges = [
+ (1, 3),
+ (3, 2),
+ (3, 4),
+ (4, 9),
+ (4, 5),
+ (3, 9),
+ (5, 8),
+ (5, 7),
+ (8, 7),
+ (7, 6),
+ ]
+ mapped = {
+ 0: "x",
+ 1: "a",
+ 2: "b",
+ 3: "c",
+ 4: "d",
+ 5: "e",
+ 6: "f",
+ 7: "g",
+ 8: "h",
+ 9: "i",
+ }
+ G1 = nx.DiGraph(edges)
+ G1.add_node(0)
+ G2 = nx.relabel_nodes(G1, mapping=mapped)
+
+ def test_updating(self):
+ G2_degree = {
+ n: (in_degree, out_degree)
+ for (n, in_degree), (_, out_degree) in zip(
+ self.G2.in_degree, self.G2.out_degree
+ )
+ }
+ gparams, sparams = _initialize_parameters(self.G1, self.G2, G2_degree)
+ m, m_rev, T1_out, T1_in, T1_tilde, _, T2_out, T2_in, T2_tilde, _ = sparams
+
+ # Add node to the mapping
+ m[4] = self.mapped[4]
+ m_rev[self.mapped[4]] = 4
+ _update_Tinout(4, self.mapped[4], gparams, sparams)
+
+ assert T1_out == {5, 9}
+ assert T1_in == {3}
+ assert T2_out == {"i", "e"}
+ assert T2_in == {"c"}
+ assert T1_tilde == {0, 1, 2, 6, 7, 8}
+ assert T2_tilde == {"x", "a", "b", "f", "g", "h"}
+
+ # Add node to the mapping
+ m[5] = self.mapped[5]
+ m_rev[self.mapped[5]] = 5
+ _update_Tinout(5, self.mapped[5], gparams, sparams)
+
+ assert T1_out == {9, 8, 7}
+ assert T1_in == {3}
+ assert T2_out == {"i", "g", "h"}
+ assert T2_in == {"c"}
+ assert T1_tilde == {0, 1, 2, 6}
+ assert T2_tilde == {"x", "a", "b", "f"}
+
+ # Add node to the mapping
+ m[6] = self.mapped[6]
+ m_rev[self.mapped[6]] = 6
+ _update_Tinout(6, self.mapped[6], gparams, sparams)
+
+ assert T1_out == {9, 8, 7}
+ assert T1_in == {3, 7}
+ assert T2_out == {"i", "g", "h"}
+ assert T2_in == {"c", "g"}
+ assert T1_tilde == {0, 1, 2}
+ assert T2_tilde == {"x", "a", "b"}
+
+ # Add node to the mapping
+ m[3] = self.mapped[3]
+ m_rev[self.mapped[3]] = 3
+ _update_Tinout(3, self.mapped[3], gparams, sparams)
+
+ assert T1_out == {9, 8, 7, 2}
+ assert T1_in == {7, 1}
+ assert T2_out == {"i", "g", "h", "b"}
+ assert T2_in == {"g", "a"}
+ assert T1_tilde == {0}
+ assert T2_tilde == {"x"}
+
+ # Add node to the mapping
+ m[0] = self.mapped[0]
+ m_rev[self.mapped[0]] = 0
+ _update_Tinout(0, self.mapped[0], gparams, sparams)
+
+ assert T1_out == {9, 8, 7, 2}
+ assert T1_in == {7, 1}
+ assert T2_out == {"i", "g", "h", "b"}
+ assert T2_in == {"g", "a"}
+ assert T1_tilde == set()
+ assert T2_tilde == set()
+
+ def test_restoring(self):
+ m = {0: "x", 3: "c", 4: "d", 5: "e", 6: "f"}
+ m_rev = {"x": 0, "c": 3, "d": 4, "e": 5, "f": 6}
+
+ T1_out = {2, 7, 9, 8}
+ T1_in = {1, 7}
+ T2_out = {"b", "g", "i", "h"}
+ T2_in = {"a", "g"}
+ T1_tilde = set()
+ T2_tilde = set()
+
+ gparams = _GraphParameters(self.G1, self.G2, {}, {}, {}, {}, {})
+ sparams = _StateParameters(
+ m, m_rev, T1_out, T1_in, T1_tilde, None, T2_out, T2_in, T2_tilde, None
+ )
+
+ # Remove a node from the mapping
+ m.pop(0)
+ m_rev.pop("x")
+ _restore_Tinout_Di(0, self.mapped[0], gparams, sparams)
+
+ assert T1_out == {2, 7, 9, 8}
+ assert T1_in == {1, 7}
+ assert T2_out == {"b", "g", "i", "h"}
+ assert T2_in == {"a", "g"}
+ assert T1_tilde == {0}
+ assert T2_tilde == {"x"}
+
+ # Remove a node from the mapping
+ m.pop(6)
+ m_rev.pop("f")
+ _restore_Tinout_Di(6, self.mapped[6], gparams, sparams)
+
+ assert T1_out == {2, 9, 8, 7}
+ assert T1_in == {1}
+ assert T2_out == {"b", "i", "h", "g"}
+ assert T2_in == {"a"}
+ assert T1_tilde == {0, 6}
+ assert T2_tilde == {"x", "f"}
+
+ # Remove a node from the mapping
+ m.pop(3)
+ m_rev.pop("c")
+ _restore_Tinout_Di(3, self.mapped[3], gparams, sparams)
+
+ assert T1_out == {9, 8, 7}
+ assert T1_in == {3}
+ assert T2_out == {"i", "h", "g"}
+ assert T2_in == {"c"}
+ assert T1_tilde == {0, 6, 1, 2}
+ assert T2_tilde == {"x", "f", "a", "b"}
+
+ # Remove a node from the mapping
+ m.pop(5)
+ m_rev.pop("e")
+ _restore_Tinout_Di(5, self.mapped[5], gparams, sparams)
+
+ assert T1_out == {9, 5}
+ assert T1_in == {3}
+ assert T2_out == {"i", "e"}
+ assert T2_in == {"c"}
+ assert T1_tilde == {0, 6, 1, 2, 8, 7}
+ assert T2_tilde == {"x", "f", "a", "b", "h", "g"}
+
+ # Remove a node from the mapping
+ m.pop(4)
+ m_rev.pop("d")
+ _restore_Tinout_Di(4, self.mapped[4], gparams, sparams)
+
+ assert T1_out == set()
+ assert T1_in == set()
+ assert T2_out == set()
+ assert T2_in == set()
+ assert T1_tilde == set(self.G1.nodes())
+ assert T2_tilde == set(self.G2.nodes())
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py
new file mode 100644
index 00000000..b44f4588
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py
@@ -0,0 +1,200 @@
+"""
+Tests for VF2 isomorphism algorithm for weighted graphs.
+"""
+
+import math
+from operator import eq
+
+import networkx as nx
+import networkx.algorithms.isomorphism as iso
+
+
+def test_simple():
+ # 16 simple tests
+ w = "weight"
+ edges = [(0, 0, 1), (0, 0, 1.5), (0, 1, 2), (1, 0, 3)]
+ for g1 in [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()]:
+ g1.add_weighted_edges_from(edges)
+ g2 = g1.subgraph(g1.nodes())
+ if g1.is_multigraph():
+ em = iso.numerical_multiedge_match("weight", 1)
+ else:
+ em = iso.numerical_edge_match("weight", 1)
+ assert nx.is_isomorphic(g1, g2, edge_match=em)
+
+ for mod1, mod2 in [(False, True), (True, False), (True, True)]:
+ # mod1 tests a regular edge
+ # mod2 tests a selfloop
+ if g2.is_multigraph():
+ if mod1:
+ data1 = {0: {"weight": 10}}
+ if mod2:
+ data2 = {0: {"weight": 1}, 1: {"weight": 2.5}}
+ else:
+ if mod1:
+ data1 = {"weight": 10}
+ if mod2:
+ data2 = {"weight": 2.5}
+
+ g2 = g1.subgraph(g1.nodes()).copy()
+ if mod1:
+ if not g1.is_directed():
+ g2._adj[1][0] = data1
+ g2._adj[0][1] = data1
+ else:
+ g2._succ[1][0] = data1
+ g2._pred[0][1] = data1
+ if mod2:
+ if not g1.is_directed():
+ g2._adj[0][0] = data2
+ else:
+ g2._succ[0][0] = data2
+ g2._pred[0][0] = data2
+
+ assert not nx.is_isomorphic(g1, g2, edge_match=em)
+
+
+def test_weightkey():
+ g1 = nx.DiGraph()
+ g2 = nx.DiGraph()
+
+ g1.add_edge("A", "B", weight=1)
+ g2.add_edge("C", "D", weight=0)
+
+ assert nx.is_isomorphic(g1, g2)
+ em = iso.numerical_edge_match("nonexistent attribute", 1)
+ assert nx.is_isomorphic(g1, g2, edge_match=em)
+ em = iso.numerical_edge_match("weight", 1)
+ assert not nx.is_isomorphic(g1, g2, edge_match=em)
+
+ g2 = nx.DiGraph()
+ g2.add_edge("C", "D")
+ assert nx.is_isomorphic(g1, g2, edge_match=em)
+
+
+class TestNodeMatch_Graph:
+ def setup_method(self):
+ self.g1 = nx.Graph()
+ self.g2 = nx.Graph()
+ self.build()
+
+ def build(self):
+ self.nm = iso.categorical_node_match("color", "")
+ self.em = iso.numerical_edge_match("weight", 1)
+
+ self.g1.add_node("A", color="red")
+ self.g2.add_node("C", color="blue")
+
+ self.g1.add_edge("A", "B", weight=1)
+ self.g2.add_edge("C", "D", weight=1)
+
+ def test_noweight_nocolor(self):
+ assert nx.is_isomorphic(self.g1, self.g2)
+
+ def test_color1(self):
+ assert not nx.is_isomorphic(self.g1, self.g2, node_match=self.nm)
+
+ def test_color2(self):
+ self.g1.nodes["A"]["color"] = "blue"
+ assert nx.is_isomorphic(self.g1, self.g2, node_match=self.nm)
+
+ def test_weight1(self):
+ assert nx.is_isomorphic(self.g1, self.g2, edge_match=self.em)
+
+ def test_weight2(self):
+ self.g1.add_edge("A", "B", weight=2)
+ assert not nx.is_isomorphic(self.g1, self.g2, edge_match=self.em)
+
+ def test_colorsandweights1(self):
+ iso = nx.is_isomorphic(self.g1, self.g2, node_match=self.nm, edge_match=self.em)
+ assert not iso
+
+ def test_colorsandweights2(self):
+ self.g1.nodes["A"]["color"] = "blue"
+ iso = nx.is_isomorphic(self.g1, self.g2, node_match=self.nm, edge_match=self.em)
+ assert iso
+
+ def test_colorsandweights3(self):
+ # make the weights disagree
+ self.g1.add_edge("A", "B", weight=2)
+ assert not nx.is_isomorphic(
+ self.g1, self.g2, node_match=self.nm, edge_match=self.em
+ )
+
+
+class TestEdgeMatch_MultiGraph:
+ def setup_method(self):
+ self.g1 = nx.MultiGraph()
+ self.g2 = nx.MultiGraph()
+ self.GM = iso.MultiGraphMatcher
+ self.build()
+
+ def build(self):
+ g1 = self.g1
+ g2 = self.g2
+
+ # We will assume integer weights only.
+ g1.add_edge("A", "B", color="green", weight=0, size=0.5)
+ g1.add_edge("A", "B", color="red", weight=1, size=0.35)
+ g1.add_edge("A", "B", color="red", weight=2, size=0.65)
+
+ g2.add_edge("C", "D", color="green", weight=1, size=0.5)
+ g2.add_edge("C", "D", color="red", weight=0, size=0.45)
+ g2.add_edge("C", "D", color="red", weight=2, size=0.65)
+
+ if g1.is_multigraph():
+ self.em = iso.numerical_multiedge_match("weight", 1)
+ self.emc = iso.categorical_multiedge_match("color", "")
+ self.emcm = iso.categorical_multiedge_match(["color", "weight"], ["", 1])
+ self.emg1 = iso.generic_multiedge_match("color", "red", eq)
+ self.emg2 = iso.generic_multiedge_match(
+ ["color", "weight", "size"],
+ ["red", 1, 0.5],
+ [eq, eq, math.isclose],
+ )
+ else:
+ self.em = iso.numerical_edge_match("weight", 1)
+ self.emc = iso.categorical_edge_match("color", "")
+ self.emcm = iso.categorical_edge_match(["color", "weight"], ["", 1])
+ self.emg1 = iso.generic_multiedge_match("color", "red", eq)
+ self.emg2 = iso.generic_edge_match(
+ ["color", "weight", "size"],
+ ["red", 1, 0.5],
+ [eq, eq, math.isclose],
+ )
+
+ def test_weights_only(self):
+ assert nx.is_isomorphic(self.g1, self.g2, edge_match=self.em)
+
+ def test_colors_only(self):
+ gm = self.GM(self.g1, self.g2, edge_match=self.emc)
+ assert gm.is_isomorphic()
+
+ def test_colorsandweights(self):
+ gm = self.GM(self.g1, self.g2, edge_match=self.emcm)
+ assert not gm.is_isomorphic()
+
+ def test_generic1(self):
+ gm = self.GM(self.g1, self.g2, edge_match=self.emg1)
+ assert gm.is_isomorphic()
+
+ def test_generic2(self):
+ gm = self.GM(self.g1, self.g2, edge_match=self.emg2)
+ assert not gm.is_isomorphic()
+
+
+class TestEdgeMatch_DiGraph(TestNodeMatch_Graph):
+ def setup_method(self):
+ TestNodeMatch_Graph.setup_method(self)
+ self.g1 = nx.DiGraph()
+ self.g2 = nx.DiGraph()
+ self.build()
+
+
+class TestEdgeMatch_MultiDiGraph(TestEdgeMatch_MultiGraph):
+ def setup_method(self):
+ TestEdgeMatch_MultiGraph.setup_method(self)
+ self.g1 = nx.MultiDiGraph()
+ self.g2 = nx.MultiDiGraph()
+ self.GM = iso.MultiDiGraphMatcher
+ self.build()