aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py686
1 files changed, 686 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py
new file mode 100644
index 00000000..0828069d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py
@@ -0,0 +1,686 @@
+import pytest
+
+import networkx as nx
+from networkx.generators import directed
+
+# Unit tests for the :func:`~networkx.weisfeiler_lehman_graph_hash` function
+
+
+def test_empty_graph_hash():
+ """
+ empty graphs should give hashes regardless of other params
+ """
+ G1 = nx.empty_graph()
+ G2 = nx.empty_graph()
+
+ h1 = nx.weisfeiler_lehman_graph_hash(G1)
+ h2 = nx.weisfeiler_lehman_graph_hash(G2)
+ h3 = nx.weisfeiler_lehman_graph_hash(G2, edge_attr="edge_attr1")
+ h4 = nx.weisfeiler_lehman_graph_hash(G2, node_attr="node_attr1")
+ h5 = nx.weisfeiler_lehman_graph_hash(
+ G2, edge_attr="edge_attr1", node_attr="node_attr1"
+ )
+ h6 = nx.weisfeiler_lehman_graph_hash(G2, iterations=10)
+
+ assert h1 == h2
+ assert h1 == h3
+ assert h1 == h4
+ assert h1 == h5
+ assert h1 == h6
+
+
+def test_directed():
+ """
+ A directed graph with no bi-directional edges should yield different a graph hash
+ to the same graph taken as undirected if there are no hash collisions.
+ """
+ r = 10
+ for i in range(r):
+ G_directed = nx.gn_graph(10 + r, seed=100 + i)
+ G_undirected = nx.to_undirected(G_directed)
+
+ h_directed = nx.weisfeiler_lehman_graph_hash(G_directed)
+ h_undirected = nx.weisfeiler_lehman_graph_hash(G_undirected)
+
+ assert h_directed != h_undirected
+
+
+def test_reversed():
+ """
+ A directed graph with no bi-directional edges should yield different a graph hash
+ to the same graph taken with edge directions reversed if there are no hash collisions.
+ Here we test a cycle graph which is the minimal counterexample
+ """
+ G = nx.cycle_graph(5, create_using=nx.DiGraph)
+ nx.set_node_attributes(G, {n: str(n) for n in G.nodes()}, name="label")
+
+ G_reversed = G.reverse()
+
+ h = nx.weisfeiler_lehman_graph_hash(G, node_attr="label")
+ h_reversed = nx.weisfeiler_lehman_graph_hash(G_reversed, node_attr="label")
+
+ assert h != h_reversed
+
+
+def test_isomorphic():
+ """
+ graph hashes should be invariant to node-relabeling (when the output is reindexed
+ by the same mapping)
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G1 = nx.erdos_renyi_graph(n, p * i, seed=200 + i)
+ G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+ g1_hash = nx.weisfeiler_lehman_graph_hash(G1)
+ g2_hash = nx.weisfeiler_lehman_graph_hash(G2)
+
+ assert g1_hash == g2_hash
+
+
+def test_isomorphic_edge_attr():
+ """
+ Isomorphic graphs with differing edge attributes should yield different graph
+ hashes if the 'edge_attr' argument is supplied and populated in the graph,
+ and there are no hash collisions.
+ The output should still be invariant to node-relabeling
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G1 = nx.erdos_renyi_graph(n, p * i, seed=300 + i)
+
+ for a, b in G1.edges:
+ G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
+ G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
+
+ g1_hash_with_edge_attr1 = nx.weisfeiler_lehman_graph_hash(
+ G1, edge_attr="edge_attr1"
+ )
+ g1_hash_with_edge_attr2 = nx.weisfeiler_lehman_graph_hash(
+ G1, edge_attr="edge_attr2"
+ )
+ g1_hash_no_edge_attr = nx.weisfeiler_lehman_graph_hash(G1, edge_attr=None)
+
+ assert g1_hash_with_edge_attr1 != g1_hash_no_edge_attr
+ assert g1_hash_with_edge_attr2 != g1_hash_no_edge_attr
+ assert g1_hash_with_edge_attr1 != g1_hash_with_edge_attr2
+
+ G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+ g2_hash_with_edge_attr1 = nx.weisfeiler_lehman_graph_hash(
+ G2, edge_attr="edge_attr1"
+ )
+ g2_hash_with_edge_attr2 = nx.weisfeiler_lehman_graph_hash(
+ G2, edge_attr="edge_attr2"
+ )
+
+ assert g1_hash_with_edge_attr1 == g2_hash_with_edge_attr1
+ assert g1_hash_with_edge_attr2 == g2_hash_with_edge_attr2
+
+
+def test_missing_edge_attr():
+ """
+ If the 'edge_attr' argument is supplied but is missing from an edge in the graph,
+ we should raise a KeyError
+ """
+ G = nx.Graph()
+ G.add_edges_from([(1, 2, {"edge_attr1": "a"}), (1, 3, {})])
+ pytest.raises(KeyError, nx.weisfeiler_lehman_graph_hash, G, edge_attr="edge_attr1")
+
+
+def test_isomorphic_node_attr():
+ """
+ Isomorphic graphs with differing node attributes should yield different graph
+ hashes if the 'node_attr' argument is supplied and populated in the graph, and
+ there are no hash collisions.
+ The output should still be invariant to node-relabeling
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G1 = nx.erdos_renyi_graph(n, p * i, seed=400 + i)
+
+ for u in G1.nodes():
+ G1.nodes[u]["node_attr1"] = f"{u}-1"
+ G1.nodes[u]["node_attr2"] = f"{u}-2"
+
+ g1_hash_with_node_attr1 = nx.weisfeiler_lehman_graph_hash(
+ G1, node_attr="node_attr1"
+ )
+ g1_hash_with_node_attr2 = nx.weisfeiler_lehman_graph_hash(
+ G1, node_attr="node_attr2"
+ )
+ g1_hash_no_node_attr = nx.weisfeiler_lehman_graph_hash(G1, node_attr=None)
+
+ assert g1_hash_with_node_attr1 != g1_hash_no_node_attr
+ assert g1_hash_with_node_attr2 != g1_hash_no_node_attr
+ assert g1_hash_with_node_attr1 != g1_hash_with_node_attr2
+
+ G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+ g2_hash_with_node_attr1 = nx.weisfeiler_lehman_graph_hash(
+ G2, node_attr="node_attr1"
+ )
+ g2_hash_with_node_attr2 = nx.weisfeiler_lehman_graph_hash(
+ G2, node_attr="node_attr2"
+ )
+
+ assert g1_hash_with_node_attr1 == g2_hash_with_node_attr1
+ assert g1_hash_with_node_attr2 == g2_hash_with_node_attr2
+
+
+def test_missing_node_attr():
+ """
+ If the 'node_attr' argument is supplied but is missing from a node in the graph,
+ we should raise a KeyError
+ """
+ G = nx.Graph()
+ G.add_nodes_from([(1, {"node_attr1": "a"}), (2, {})])
+ G.add_edges_from([(1, 2), (2, 3), (3, 1), (1, 4)])
+ pytest.raises(KeyError, nx.weisfeiler_lehman_graph_hash, G, node_attr="node_attr1")
+
+
+def test_isomorphic_edge_attr_and_node_attr():
+ """
+ Isomorphic graphs with differing node attributes should yield different graph
+ hashes if the 'node_attr' and 'edge_attr' argument is supplied and populated in
+ the graph, and there are no hash collisions.
+ The output should still be invariant to node-relabeling
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i)
+
+ for u in G1.nodes():
+ G1.nodes[u]["node_attr1"] = f"{u}-1"
+ G1.nodes[u]["node_attr2"] = f"{u}-2"
+
+ for a, b in G1.edges:
+ G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
+ G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
+
+ g1_hash_edge1_node1 = nx.weisfeiler_lehman_graph_hash(
+ G1, edge_attr="edge_attr1", node_attr="node_attr1"
+ )
+ g1_hash_edge2_node2 = nx.weisfeiler_lehman_graph_hash(
+ G1, edge_attr="edge_attr2", node_attr="node_attr2"
+ )
+ g1_hash_edge1_node2 = nx.weisfeiler_lehman_graph_hash(
+ G1, edge_attr="edge_attr1", node_attr="node_attr2"
+ )
+ g1_hash_no_attr = nx.weisfeiler_lehman_graph_hash(G1)
+
+ assert g1_hash_edge1_node1 != g1_hash_no_attr
+ assert g1_hash_edge2_node2 != g1_hash_no_attr
+ assert g1_hash_edge1_node1 != g1_hash_edge2_node2
+ assert g1_hash_edge1_node2 != g1_hash_edge2_node2
+ assert g1_hash_edge1_node2 != g1_hash_edge1_node1
+
+ G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+ g2_hash_edge1_node1 = nx.weisfeiler_lehman_graph_hash(
+ G2, edge_attr="edge_attr1", node_attr="node_attr1"
+ )
+ g2_hash_edge2_node2 = nx.weisfeiler_lehman_graph_hash(
+ G2, edge_attr="edge_attr2", node_attr="node_attr2"
+ )
+
+ assert g1_hash_edge1_node1 == g2_hash_edge1_node1
+ assert g1_hash_edge2_node2 == g2_hash_edge2_node2
+
+
+def test_digest_size():
+ """
+ The hash string lengths should be as expected for a variety of graphs and
+ digest sizes
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G = nx.erdos_renyi_graph(n, p * i, seed=1000 + i)
+
+ h16 = nx.weisfeiler_lehman_graph_hash(G)
+ h32 = nx.weisfeiler_lehman_graph_hash(G, digest_size=32)
+
+ assert h16 != h32
+ assert len(h16) == 16 * 2
+ assert len(h32) == 32 * 2
+
+
+# Unit tests for the :func:`~networkx.weisfeiler_lehman_hash_subgraphs` function
+
+
+def is_subiteration(a, b):
+ """
+ returns True if that each hash sequence in 'a' is a prefix for
+ the corresponding sequence indexed by the same node in 'b'.
+ """
+ return all(b[node][: len(hashes)] == hashes for node, hashes in a.items())
+
+
+def hexdigest_sizes_correct(a, digest_size):
+ """
+ returns True if all hex digest sizes are the expected length in a node:subgraph-hashes
+ dictionary. Hex digest string length == 2 * bytes digest length since each pair of hex
+ digits encodes 1 byte (https://docs.python.org/3/library/hashlib.html)
+ """
+ hexdigest_size = digest_size * 2
+ list_digest_sizes_correct = lambda l: all(len(x) == hexdigest_size for x in l)
+ return all(list_digest_sizes_correct(hashes) for hashes in a.values())
+
+
+def test_empty_graph_subgraph_hash():
+ """ "
+ empty graphs should give empty dict subgraph hashes regardless of other params
+ """
+ G = nx.empty_graph()
+
+ subgraph_hashes1 = nx.weisfeiler_lehman_subgraph_hashes(G)
+ subgraph_hashes2 = nx.weisfeiler_lehman_subgraph_hashes(G, edge_attr="edge_attr")
+ subgraph_hashes3 = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="edge_attr")
+ subgraph_hashes4 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=2)
+ subgraph_hashes5 = nx.weisfeiler_lehman_subgraph_hashes(G, digest_size=64)
+
+ assert subgraph_hashes1 == {}
+ assert subgraph_hashes2 == {}
+ assert subgraph_hashes3 == {}
+ assert subgraph_hashes4 == {}
+ assert subgraph_hashes5 == {}
+
+
+def test_directed_subgraph_hash():
+ """
+ A directed graph with no bi-directional edges should yield different subgraph hashes
+ to the same graph taken as undirected, if all hashes don't collide.
+ """
+ r = 10
+ for i in range(r):
+ G_directed = nx.gn_graph(10 + r, seed=100 + i)
+ G_undirected = nx.to_undirected(G_directed)
+
+ directed_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G_directed)
+ undirected_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G_undirected)
+
+ assert directed_subgraph_hashes != undirected_subgraph_hashes
+
+
+def test_reversed_subgraph_hash():
+ """
+ A directed graph with no bi-directional edges should yield different subgraph hashes
+ to the same graph taken with edge directions reversed if there are no hash collisions.
+ Here we test a cycle graph which is the minimal counterexample
+ """
+ G = nx.cycle_graph(5, create_using=nx.DiGraph)
+ nx.set_node_attributes(G, {n: str(n) for n in G.nodes()}, name="label")
+
+ G_reversed = G.reverse()
+
+ h = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="label")
+ h_reversed = nx.weisfeiler_lehman_subgraph_hashes(G_reversed, node_attr="label")
+
+ assert h != h_reversed
+
+
+def test_isomorphic_subgraph_hash():
+ """
+ the subgraph hashes should be invariant to node-relabeling when the output is reindexed
+ by the same mapping and all hashes don't collide.
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G1 = nx.erdos_renyi_graph(n, p * i, seed=200 + i)
+ G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+ g1_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G1)
+ g2_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G2)
+
+ assert g1_subgraph_hashes == {-1 * k: v for k, v in g2_subgraph_hashes.items()}
+
+
+def test_isomorphic_edge_attr_subgraph_hash():
+ """
+ Isomorphic graphs with differing edge attributes should yield different subgraph
+ hashes if the 'edge_attr' argument is supplied and populated in the graph, and
+ all hashes don't collide.
+ The output should still be invariant to node-relabeling
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G1 = nx.erdos_renyi_graph(n, p * i, seed=300 + i)
+
+ for a, b in G1.edges:
+ G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
+ G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
+
+ g1_hash_with_edge_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
+ G1, edge_attr="edge_attr1"
+ )
+ g1_hash_with_edge_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
+ G1, edge_attr="edge_attr2"
+ )
+ g1_hash_no_edge_attr = nx.weisfeiler_lehman_subgraph_hashes(G1, edge_attr=None)
+
+ assert g1_hash_with_edge_attr1 != g1_hash_no_edge_attr
+ assert g1_hash_with_edge_attr2 != g1_hash_no_edge_attr
+ assert g1_hash_with_edge_attr1 != g1_hash_with_edge_attr2
+
+ G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+ g2_hash_with_edge_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
+ G2, edge_attr="edge_attr1"
+ )
+ g2_hash_with_edge_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
+ G2, edge_attr="edge_attr2"
+ )
+
+ assert g1_hash_with_edge_attr1 == {
+ -1 * k: v for k, v in g2_hash_with_edge_attr1.items()
+ }
+ assert g1_hash_with_edge_attr2 == {
+ -1 * k: v for k, v in g2_hash_with_edge_attr2.items()
+ }
+
+
+def test_missing_edge_attr_subgraph_hash():
+ """
+ If the 'edge_attr' argument is supplied but is missing from an edge in the graph,
+ we should raise a KeyError
+ """
+ G = nx.Graph()
+ G.add_edges_from([(1, 2, {"edge_attr1": "a"}), (1, 3, {})])
+ pytest.raises(
+ KeyError, nx.weisfeiler_lehman_subgraph_hashes, G, edge_attr="edge_attr1"
+ )
+
+
+def test_isomorphic_node_attr_subgraph_hash():
+ """
+ Isomorphic graphs with differing node attributes should yield different subgraph
+ hashes if the 'node_attr' argument is supplied and populated in the graph, and
+ all hashes don't collide.
+ The output should still be invariant to node-relabeling
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G1 = nx.erdos_renyi_graph(n, p * i, seed=400 + i)
+
+ for u in G1.nodes():
+ G1.nodes[u]["node_attr1"] = f"{u}-1"
+ G1.nodes[u]["node_attr2"] = f"{u}-2"
+
+ g1_hash_with_node_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
+ G1, node_attr="node_attr1"
+ )
+ g1_hash_with_node_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
+ G1, node_attr="node_attr2"
+ )
+ g1_hash_no_node_attr = nx.weisfeiler_lehman_subgraph_hashes(G1, node_attr=None)
+
+ assert g1_hash_with_node_attr1 != g1_hash_no_node_attr
+ assert g1_hash_with_node_attr2 != g1_hash_no_node_attr
+ assert g1_hash_with_node_attr1 != g1_hash_with_node_attr2
+
+ G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+ g2_hash_with_node_attr1 = nx.weisfeiler_lehman_subgraph_hashes(
+ G2, node_attr="node_attr1"
+ )
+ g2_hash_with_node_attr2 = nx.weisfeiler_lehman_subgraph_hashes(
+ G2, node_attr="node_attr2"
+ )
+
+ assert g1_hash_with_node_attr1 == {
+ -1 * k: v for k, v in g2_hash_with_node_attr1.items()
+ }
+ assert g1_hash_with_node_attr2 == {
+ -1 * k: v for k, v in g2_hash_with_node_attr2.items()
+ }
+
+
+def test_missing_node_attr_subgraph_hash():
+ """
+ If the 'node_attr' argument is supplied but is missing from a node in the graph,
+ we should raise a KeyError
+ """
+ G = nx.Graph()
+ G.add_nodes_from([(1, {"node_attr1": "a"}), (2, {})])
+ G.add_edges_from([(1, 2), (2, 3), (3, 1), (1, 4)])
+ pytest.raises(
+ KeyError, nx.weisfeiler_lehman_subgraph_hashes, G, node_attr="node_attr1"
+ )
+
+
+def test_isomorphic_edge_attr_and_node_attr_subgraph_hash():
+ """
+ Isomorphic graphs with differing node attributes should yield different subgraph
+ hashes if the 'node_attr' and 'edge_attr' argument is supplied and populated in
+ the graph, and all hashes don't collide
+ The output should still be invariant to node-relabeling
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i)
+
+ for u in G1.nodes():
+ G1.nodes[u]["node_attr1"] = f"{u}-1"
+ G1.nodes[u]["node_attr2"] = f"{u}-2"
+
+ for a, b in G1.edges:
+ G1[a][b]["edge_attr1"] = f"{a}-{b}-1"
+ G1[a][b]["edge_attr2"] = f"{a}-{b}-2"
+
+ g1_hash_edge1_node1 = nx.weisfeiler_lehman_subgraph_hashes(
+ G1, edge_attr="edge_attr1", node_attr="node_attr1"
+ )
+ g1_hash_edge2_node2 = nx.weisfeiler_lehman_subgraph_hashes(
+ G1, edge_attr="edge_attr2", node_attr="node_attr2"
+ )
+ g1_hash_edge1_node2 = nx.weisfeiler_lehman_subgraph_hashes(
+ G1, edge_attr="edge_attr1", node_attr="node_attr2"
+ )
+ g1_hash_no_attr = nx.weisfeiler_lehman_subgraph_hashes(G1)
+
+ assert g1_hash_edge1_node1 != g1_hash_no_attr
+ assert g1_hash_edge2_node2 != g1_hash_no_attr
+ assert g1_hash_edge1_node1 != g1_hash_edge2_node2
+ assert g1_hash_edge1_node2 != g1_hash_edge2_node2
+ assert g1_hash_edge1_node2 != g1_hash_edge1_node1
+
+ G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()})
+
+ g2_hash_edge1_node1 = nx.weisfeiler_lehman_subgraph_hashes(
+ G2, edge_attr="edge_attr1", node_attr="node_attr1"
+ )
+ g2_hash_edge2_node2 = nx.weisfeiler_lehman_subgraph_hashes(
+ G2, edge_attr="edge_attr2", node_attr="node_attr2"
+ )
+
+ assert g1_hash_edge1_node1 == {
+ -1 * k: v for k, v in g2_hash_edge1_node1.items()
+ }
+ assert g1_hash_edge2_node2 == {
+ -1 * k: v for k, v in g2_hash_edge2_node2.items()
+ }
+
+
+def test_iteration_depth():
+ """
+ All nodes should have the correct number of subgraph hashes in the output when
+ using degree as initial node labels
+ Subsequent iteration depths for the same graph should be additive for each node
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G = nx.erdos_renyi_graph(n, p * i, seed=600 + i)
+
+ depth3 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=3)
+ depth4 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=4)
+ depth5 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=5)
+
+ assert all(len(hashes) == 3 for hashes in depth3.values())
+ assert all(len(hashes) == 4 for hashes in depth4.values())
+ assert all(len(hashes) == 5 for hashes in depth5.values())
+
+ assert is_subiteration(depth3, depth4)
+ assert is_subiteration(depth4, depth5)
+ assert is_subiteration(depth3, depth5)
+
+
+def test_iteration_depth_edge_attr():
+ """
+ All nodes should have the correct number of subgraph hashes in the output when
+ setting initial node labels empty and using an edge attribute when aggregating
+ neighborhoods.
+ Subsequent iteration depths for the same graph should be additive for each node
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G = nx.erdos_renyi_graph(n, p * i, seed=700 + i)
+
+ for a, b in G.edges:
+ G[a][b]["edge_attr1"] = f"{a}-{b}-1"
+
+ depth3 = nx.weisfeiler_lehman_subgraph_hashes(
+ G, edge_attr="edge_attr1", iterations=3
+ )
+ depth4 = nx.weisfeiler_lehman_subgraph_hashes(
+ G, edge_attr="edge_attr1", iterations=4
+ )
+ depth5 = nx.weisfeiler_lehman_subgraph_hashes(
+ G, edge_attr="edge_attr1", iterations=5
+ )
+
+ assert all(len(hashes) == 3 for hashes in depth3.values())
+ assert all(len(hashes) == 4 for hashes in depth4.values())
+ assert all(len(hashes) == 5 for hashes in depth5.values())
+
+ assert is_subiteration(depth3, depth4)
+ assert is_subiteration(depth4, depth5)
+ assert is_subiteration(depth3, depth5)
+
+
+def test_iteration_depth_node_attr():
+ """
+ All nodes should have the correct number of subgraph hashes in the output when
+ setting initial node labels to an attribute.
+ Subsequent iteration depths for the same graph should be additive for each node
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G = nx.erdos_renyi_graph(n, p * i, seed=800 + i)
+
+ for u in G.nodes():
+ G.nodes[u]["node_attr1"] = f"{u}-1"
+
+ depth3 = nx.weisfeiler_lehman_subgraph_hashes(
+ G, node_attr="node_attr1", iterations=3
+ )
+ depth4 = nx.weisfeiler_lehman_subgraph_hashes(
+ G, node_attr="node_attr1", iterations=4
+ )
+ depth5 = nx.weisfeiler_lehman_subgraph_hashes(
+ G, node_attr="node_attr1", iterations=5
+ )
+
+ assert all(len(hashes) == 3 for hashes in depth3.values())
+ assert all(len(hashes) == 4 for hashes in depth4.values())
+ assert all(len(hashes) == 5 for hashes in depth5.values())
+
+ assert is_subiteration(depth3, depth4)
+ assert is_subiteration(depth4, depth5)
+ assert is_subiteration(depth3, depth5)
+
+
+def test_iteration_depth_node_edge_attr():
+ """
+ All nodes should have the correct number of subgraph hashes in the output when
+ setting initial node labels to an attribute and also using an edge attribute when
+ aggregating neighborhoods.
+ Subsequent iteration depths for the same graph should be additive for each node
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G = nx.erdos_renyi_graph(n, p * i, seed=900 + i)
+
+ for u in G.nodes():
+ G.nodes[u]["node_attr1"] = f"{u}-1"
+
+ for a, b in G.edges:
+ G[a][b]["edge_attr1"] = f"{a}-{b}-1"
+
+ depth3 = nx.weisfeiler_lehman_subgraph_hashes(
+ G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=3
+ )
+ depth4 = nx.weisfeiler_lehman_subgraph_hashes(
+ G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=4
+ )
+ depth5 = nx.weisfeiler_lehman_subgraph_hashes(
+ G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=5
+ )
+
+ assert all(len(hashes) == 3 for hashes in depth3.values())
+ assert all(len(hashes) == 4 for hashes in depth4.values())
+ assert all(len(hashes) == 5 for hashes in depth5.values())
+
+ assert is_subiteration(depth3, depth4)
+ assert is_subiteration(depth4, depth5)
+ assert is_subiteration(depth3, depth5)
+
+
+def test_digest_size_subgraph_hash():
+ """
+ The hash string lengths should be as expected for a variety of graphs and
+ digest sizes
+ """
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(1, r + 1):
+ G = nx.erdos_renyi_graph(n, p * i, seed=1000 + i)
+
+ digest_size16_hashes = nx.weisfeiler_lehman_subgraph_hashes(G)
+ digest_size32_hashes = nx.weisfeiler_lehman_subgraph_hashes(G, digest_size=32)
+
+ assert digest_size16_hashes != digest_size32_hashes
+
+ assert hexdigest_sizes_correct(digest_size16_hashes, 16)
+ assert hexdigest_sizes_correct(digest_size32_hashes, 32)
+
+
+def test_initial_node_labels_subgraph_hash():
+ """
+ Including the hashed initial label prepends an extra hash to the lists
+ """
+ G = nx.path_graph(5)
+ nx.set_node_attributes(G, {i: int(0 < i < 4) for i in G}, "label")
+ # initial node labels:
+ # 0--1--1--1--0
+
+ without_initial_label = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="label")
+ assert all(len(v) == 3 for v in without_initial_label.values())
+ # 3 different 1 hop nhds
+ assert len({v[0] for v in without_initial_label.values()}) == 3
+
+ with_initial_label = nx.weisfeiler_lehman_subgraph_hashes(
+ G, node_attr="label", include_initial_labels=True
+ )
+ assert all(len(v) == 4 for v in with_initial_label.values())
+ # 2 different initial labels
+ assert len({v[0] for v in with_initial_label.values()}) == 2
+
+ # check hashes match otherwise
+ for u in G:
+ for a, b in zip(
+ with_initial_label[u][1:], without_initial_label[u], strict=True
+ ):
+ assert a == b