aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/operators
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/networkx/algorithms/operators
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/operators')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/__init__.py4
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/all.py321
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/binary.py450
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/product.py633
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_all.py328
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_binary.py453
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_product.py491
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_unary.py55
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/operators/unary.py77
10 files changed, 2812 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/__init__.py
new file mode 100644
index 00000000..0ebc6ab9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/__init__.py
@@ -0,0 +1,4 @@
+from networkx.algorithms.operators.all import *
+from networkx.algorithms.operators.binary import *
+from networkx.algorithms.operators.product import *
+from networkx.algorithms.operators.unary import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/all.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/all.py
new file mode 100644
index 00000000..549d335d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/all.py
@@ -0,0 +1,321 @@
+"""Operations on many graphs."""
+
+from itertools import chain, repeat
+
+import networkx as nx
+
+__all__ = ["union_all", "compose_all", "disjoint_union_all", "intersection_all"]
+
+
+@nx._dispatchable(graphs="[graphs]", preserve_all_attrs=True, returns_graph=True)
+def union_all(graphs, rename=()):
+ """Returns the union of all graphs.
+
+ The graphs must be disjoint, otherwise an exception is raised.
+
+ Parameters
+ ----------
+ graphs : iterable
+ Iterable of NetworkX graphs
+
+ rename : iterable , optional
+ Node names of graphs can be changed by specifying the tuple
+ rename=('G-','H-') (for example). Node "u" in G is then renamed
+ "G-u" and "v" in H is renamed "H-v". Infinite generators (like itertools.count)
+ are also supported.
+
+ Returns
+ -------
+ U : a graph with the same type as the first graph in list
+
+ Raises
+ ------
+ ValueError
+ If `graphs` is an empty list.
+
+ NetworkXError
+ In case of mixed type graphs, like MultiGraph and Graph, or directed and undirected graphs.
+
+ Notes
+ -----
+ For operating on mixed type graphs, they should be converted to the same type.
+ >>> G = nx.Graph()
+ >>> H = nx.DiGraph()
+ >>> GH = union_all([nx.DiGraph(G), H])
+
+ To force a disjoint union with node relabeling, use
+ disjoint_union_all(G,H) or convert_node_labels_to integers().
+
+ Graph, edge, and node attributes are propagated to the union graph.
+ If a graph attribute is present in multiple graphs, then the value
+ from the last graph in the list with that attribute is used.
+
+ Examples
+ --------
+ >>> G1 = nx.Graph([(1, 2), (2, 3)])
+ >>> G2 = nx.Graph([(4, 5), (5, 6)])
+ >>> result_graph = nx.union_all([G1, G2])
+ >>> result_graph.nodes()
+ NodeView((1, 2, 3, 4, 5, 6))
+ >>> result_graph.edges()
+ EdgeView([(1, 2), (2, 3), (4, 5), (5, 6)])
+
+ See Also
+ --------
+ union
+ disjoint_union_all
+ """
+ R = None
+ seen_nodes = set()
+
+ # rename graph to obtain disjoint node labels
+ def add_prefix(graph, prefix):
+ if prefix is None:
+ return graph
+
+ def label(x):
+ return f"{prefix}{x}"
+
+ return nx.relabel_nodes(graph, label)
+
+ rename = chain(rename, repeat(None))
+ graphs = (add_prefix(G, name) for G, name in zip(graphs, rename))
+
+ for i, G in enumerate(graphs):
+ G_nodes_set = set(G.nodes)
+ if i == 0:
+ # Union is the same type as first graph
+ R = G.__class__()
+ elif G.is_directed() != R.is_directed():
+ raise nx.NetworkXError("All graphs must be directed or undirected.")
+ elif G.is_multigraph() != R.is_multigraph():
+ raise nx.NetworkXError("All graphs must be graphs or multigraphs.")
+ elif not seen_nodes.isdisjoint(G_nodes_set):
+ raise nx.NetworkXError(
+ "The node sets of the graphs are not disjoint.\n"
+ "Use `rename` to specify prefixes for the graphs or use\n"
+ "disjoint_union(G1, G2, ..., GN)."
+ )
+
+ seen_nodes |= G_nodes_set
+ R.graph.update(G.graph)
+ R.add_nodes_from(G.nodes(data=True))
+ R.add_edges_from(
+ G.edges(keys=True, data=True) if G.is_multigraph() else G.edges(data=True)
+ )
+
+ if R is None:
+ raise ValueError("cannot apply union_all to an empty list")
+
+ return R
+
+
+@nx._dispatchable(graphs="[graphs]", preserve_all_attrs=True, returns_graph=True)
+def disjoint_union_all(graphs):
+ """Returns the disjoint union of all graphs.
+
+ This operation forces distinct integer node labels starting with 0
+ for the first graph in the list and numbering consecutively.
+
+ Parameters
+ ----------
+ graphs : iterable
+ Iterable of NetworkX graphs
+
+ Returns
+ -------
+ U : A graph with the same type as the first graph in list
+
+ Raises
+ ------
+ ValueError
+ If `graphs` is an empty list.
+
+ NetworkXError
+ In case of mixed type graphs, like MultiGraph and Graph, or directed and undirected graphs.
+
+ Examples
+ --------
+ >>> G1 = nx.Graph([(1, 2), (2, 3)])
+ >>> G2 = nx.Graph([(4, 5), (5, 6)])
+ >>> U = nx.disjoint_union_all([G1, G2])
+ >>> list(U.nodes())
+ [0, 1, 2, 3, 4, 5]
+ >>> list(U.edges())
+ [(0, 1), (1, 2), (3, 4), (4, 5)]
+
+ Notes
+ -----
+ For operating on mixed type graphs, they should be converted to the same type.
+
+ Graph, edge, and node attributes are propagated to the union graph.
+ If a graph attribute is present in multiple graphs, then the value
+ from the last graph in the list with that attribute is used.
+ """
+
+ def yield_relabeled(graphs):
+ first_label = 0
+ for G in graphs:
+ yield nx.convert_node_labels_to_integers(G, first_label=first_label)
+ first_label += len(G)
+
+ R = union_all(yield_relabeled(graphs))
+
+ return R
+
+
+@nx._dispatchable(graphs="[graphs]", preserve_all_attrs=True, returns_graph=True)
+def compose_all(graphs):
+ """Returns the composition of all graphs.
+
+ Composition is the simple union of the node sets and edge sets.
+ The node sets of the supplied graphs need not be disjoint.
+
+ Parameters
+ ----------
+ graphs : iterable
+ Iterable of NetworkX graphs
+
+ Returns
+ -------
+ C : A graph with the same type as the first graph in list
+
+ Raises
+ ------
+ ValueError
+ If `graphs` is an empty list.
+
+ NetworkXError
+ In case of mixed type graphs, like MultiGraph and Graph, or directed and undirected graphs.
+
+ Examples
+ --------
+ >>> G1 = nx.Graph([(1, 2), (2, 3)])
+ >>> G2 = nx.Graph([(3, 4), (5, 6)])
+ >>> C = nx.compose_all([G1, G2])
+ >>> list(C.nodes())
+ [1, 2, 3, 4, 5, 6]
+ >>> list(C.edges())
+ [(1, 2), (2, 3), (3, 4), (5, 6)]
+
+ Notes
+ -----
+ For operating on mixed type graphs, they should be converted to the same type.
+
+ Graph, edge, and node attributes are propagated to the union graph.
+ If a graph attribute is present in multiple graphs, then the value
+ from the last graph in the list with that attribute is used.
+ """
+ R = None
+
+ # add graph attributes, H attributes take precedent over G attributes
+ for i, G in enumerate(graphs):
+ if i == 0:
+ # create new graph
+ R = G.__class__()
+ elif G.is_directed() != R.is_directed():
+ raise nx.NetworkXError("All graphs must be directed or undirected.")
+ elif G.is_multigraph() != R.is_multigraph():
+ raise nx.NetworkXError("All graphs must be graphs or multigraphs.")
+
+ R.graph.update(G.graph)
+ R.add_nodes_from(G.nodes(data=True))
+ R.add_edges_from(
+ G.edges(keys=True, data=True) if G.is_multigraph() else G.edges(data=True)
+ )
+
+ if R is None:
+ raise ValueError("cannot apply compose_all to an empty list")
+
+ return R
+
+
+@nx._dispatchable(graphs="[graphs]", returns_graph=True)
+def intersection_all(graphs):
+ """Returns a new graph that contains only the nodes and the edges that exist in
+ all graphs.
+
+ Parameters
+ ----------
+ graphs : iterable
+ Iterable of NetworkX graphs
+
+ Returns
+ -------
+ R : A new graph with the same type as the first graph in list
+
+ Raises
+ ------
+ ValueError
+ If `graphs` is an empty list.
+
+ NetworkXError
+ In case of mixed type graphs, like MultiGraph and Graph, or directed and undirected graphs.
+
+ Notes
+ -----
+ For operating on mixed type graphs, they should be converted to the same type.
+
+ Attributes from the graph, nodes, and edges are not copied to the new
+ graph.
+
+ The resulting graph can be updated with attributes if desired. For example, code which adds the minimum attribute for each node across all graphs could work.
+ >>> g = nx.Graph()
+ >>> g.add_node(0, capacity=4)
+ >>> g.add_node(1, capacity=3)
+ >>> g.add_edge(0, 1)
+
+ >>> h = g.copy()
+ >>> h.nodes[0]["capacity"] = 2
+
+ >>> gh = nx.intersection_all([g, h])
+
+ >>> new_node_attr = {
+ ... n: min(*(anyG.nodes[n].get("capacity", float("inf")) for anyG in [g, h]))
+ ... for n in gh
+ ... }
+ >>> nx.set_node_attributes(gh, new_node_attr, "new_capacity")
+ >>> gh.nodes(data=True)
+ NodeDataView({0: {'new_capacity': 2}, 1: {'new_capacity': 3}})
+
+ Examples
+ --------
+ >>> G1 = nx.Graph([(1, 2), (2, 3)])
+ >>> G2 = nx.Graph([(2, 3), (3, 4)])
+ >>> R = nx.intersection_all([G1, G2])
+ >>> list(R.nodes())
+ [2, 3]
+ >>> list(R.edges())
+ [(2, 3)]
+
+ """
+ R = None
+
+ for i, G in enumerate(graphs):
+ G_nodes_set = set(G.nodes)
+ G_edges_set = set(G.edges)
+ if not G.is_directed():
+ if G.is_multigraph():
+ G_edges_set.update((v, u, k) for u, v, k in list(G_edges_set))
+ else:
+ G_edges_set.update((v, u) for u, v in list(G_edges_set))
+ if i == 0:
+ # create new graph
+ R = G.__class__()
+ node_intersection = G_nodes_set
+ edge_intersection = G_edges_set
+ elif G.is_directed() != R.is_directed():
+ raise nx.NetworkXError("All graphs must be directed or undirected.")
+ elif G.is_multigraph() != R.is_multigraph():
+ raise nx.NetworkXError("All graphs must be graphs or multigraphs.")
+ else:
+ node_intersection &= G_nodes_set
+ edge_intersection &= G_edges_set
+
+ if R is None:
+ raise ValueError("cannot apply intersection_all to an empty list")
+
+ R.add_nodes_from(node_intersection)
+ R.add_edges_from(edge_intersection)
+
+ return R
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/binary.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/binary.py
new file mode 100644
index 00000000..08907bf6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/binary.py
@@ -0,0 +1,450 @@
+"""
+Operations on graphs including union, intersection, difference.
+"""
+
+import networkx as nx
+
+__all__ = [
+ "union",
+ "compose",
+ "disjoint_union",
+ "intersection",
+ "difference",
+ "symmetric_difference",
+ "full_join",
+]
+_G_H = {"G": 0, "H": 1}
+
+
+@nx._dispatchable(graphs=_G_H, preserve_all_attrs=True, returns_graph=True)
+def union(G, H, rename=()):
+ """Combine graphs G and H. The names of nodes must be unique.
+
+ A name collision between the graphs will raise an exception.
+
+ A renaming facility is provided to avoid name collisions.
+
+
+ Parameters
+ ----------
+ G, H : graph
+ A NetworkX graph
+
+ rename : iterable , optional
+ Node names of G and H can be changed by specifying the tuple
+ rename=('G-','H-') (for example). Node "u" in G is then renamed
+ "G-u" and "v" in H is renamed "H-v".
+
+ Returns
+ -------
+ U : A union graph with the same type as G.
+
+ See Also
+ --------
+ compose
+ :func:`~networkx.Graph.update`
+ disjoint_union
+
+ Notes
+ -----
+ To combine graphs that have common nodes, consider compose(G, H)
+ or the method, Graph.update().
+
+ disjoint_union() is similar to union() except that it avoids name clashes
+ by relabeling the nodes with sequential integers.
+
+ Edge and node attributes are propagated from G and H to the union graph.
+ Graph attributes are also propagated, but if they are present in both G and H,
+ then the value from H is used.
+
+ Examples
+ --------
+ >>> G = nx.Graph([(0, 1), (0, 2), (1, 2)])
+ >>> H = nx.Graph([(0, 1), (0, 3), (1, 3), (1, 2)])
+ >>> U = nx.union(G, H, rename=("G", "H"))
+ >>> U.nodes
+ NodeView(('G0', 'G1', 'G2', 'H0', 'H1', 'H3', 'H2'))
+ >>> U.edges
+ EdgeView([('G0', 'G1'), ('G0', 'G2'), ('G1', 'G2'), ('H0', 'H1'), ('H0', 'H3'), ('H1', 'H3'), ('H1', 'H2')])
+
+
+ """
+ return nx.union_all([G, H], rename)
+
+
+@nx._dispatchable(graphs=_G_H, preserve_all_attrs=True, returns_graph=True)
+def disjoint_union(G, H):
+ """Combine graphs G and H. The nodes are assumed to be unique (disjoint).
+
+ This algorithm automatically relabels nodes to avoid name collisions.
+
+ Parameters
+ ----------
+ G,H : graph
+ A NetworkX graph
+
+ Returns
+ -------
+ U : A union graph with the same type as G.
+
+ See Also
+ --------
+ union
+ compose
+ :func:`~networkx.Graph.update`
+
+ Notes
+ -----
+ A new graph is created, of the same class as G. It is recommended
+ that G and H be either both directed or both undirected.
+
+ The nodes of G are relabeled 0 to len(G)-1, and the nodes of H are
+ relabeled len(G) to len(G)+len(H)-1.
+
+ Renumbering forces G and H to be disjoint, so no exception is ever raised for a name collision.
+ To preserve the check for common nodes, use union().
+
+ Edge and node attributes are propagated from G and H to the union graph.
+ Graph attributes are also propagated, but if they are present in both G and H,
+ then the value from H is used.
+
+ To combine graphs that have common nodes, consider compose(G, H)
+ or the method, Graph.update().
+
+ Examples
+ --------
+ >>> G = nx.Graph([(0, 1), (0, 2), (1, 2)])
+ >>> H = nx.Graph([(0, 3), (1, 2), (2, 3)])
+ >>> G.nodes[0]["key1"] = 5
+ >>> H.nodes[0]["key2"] = 10
+ >>> U = nx.disjoint_union(G, H)
+ >>> U.nodes(data=True)
+ NodeDataView({0: {'key1': 5}, 1: {}, 2: {}, 3: {'key2': 10}, 4: {}, 5: {}, 6: {}})
+ >>> U.edges
+ EdgeView([(0, 1), (0, 2), (1, 2), (3, 4), (4, 6), (5, 6)])
+ """
+ return nx.disjoint_union_all([G, H])
+
+
+@nx._dispatchable(graphs=_G_H, returns_graph=True)
+def intersection(G, H):
+ """Returns a new graph that contains only the nodes and the edges that exist in
+ both G and H.
+
+ Parameters
+ ----------
+ G,H : graph
+ A NetworkX graph. G and H can have different node sets but must be both graphs or both multigraphs.
+
+ Raises
+ ------
+ NetworkXError
+ If one is a MultiGraph and the other one is a graph.
+
+ Returns
+ -------
+ GH : A new graph with the same type as G.
+
+ Notes
+ -----
+ Attributes from the graph, nodes, and edges are not copied to the new
+ graph. If you want a new graph of the intersection of G and H
+ with the attributes (including edge data) from G use remove_nodes_from()
+ as follows
+
+ >>> G = nx.path_graph(3)
+ >>> H = nx.path_graph(5)
+ >>> R = G.copy()
+ >>> R.remove_nodes_from(n for n in G if n not in H)
+ >>> R.remove_edges_from(e for e in G.edges if e not in H.edges)
+
+ Examples
+ --------
+ >>> G = nx.Graph([(0, 1), (0, 2), (1, 2)])
+ >>> H = nx.Graph([(0, 3), (1, 2), (2, 3)])
+ >>> R = nx.intersection(G, H)
+ >>> R.nodes
+ NodeView((0, 1, 2))
+ >>> R.edges
+ EdgeView([(1, 2)])
+ """
+ return nx.intersection_all([G, H])
+
+
+@nx._dispatchable(graphs=_G_H, returns_graph=True)
+def difference(G, H):
+ """Returns a new graph that contains the edges that exist in G but not in H.
+
+ The node sets of H and G must be the same.
+
+ Parameters
+ ----------
+ G,H : graph
+ A NetworkX graph. G and H must have the same node sets.
+
+ Returns
+ -------
+ D : A new graph with the same type as G.
+
+ Notes
+ -----
+ Attributes from the graph, nodes, and edges are not copied to the new
+ graph. If you want a new graph of the difference of G and H with
+ the attributes (including edge data) from G use remove_nodes_from()
+ as follows:
+
+ >>> G = nx.path_graph(3)
+ >>> H = nx.path_graph(5)
+ >>> R = G.copy()
+ >>> R.remove_nodes_from(n for n in G if n in H)
+
+ Examples
+ --------
+ >>> G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3)])
+ >>> H = nx.Graph([(0, 1), (1, 2), (0, 3)])
+ >>> R = nx.difference(G, H)
+ >>> R.nodes
+ NodeView((0, 1, 2, 3))
+ >>> R.edges
+ EdgeView([(0, 2), (1, 3)])
+ """
+ # create new graph
+ if not G.is_multigraph() == H.is_multigraph():
+ raise nx.NetworkXError("G and H must both be graphs or multigraphs.")
+ R = nx.create_empty_copy(G, with_data=False)
+
+ if set(G) != set(H):
+ raise nx.NetworkXError("Node sets of graphs not equal")
+
+ if G.is_multigraph():
+ edges = G.edges(keys=True)
+ else:
+ edges = G.edges()
+ for e in edges:
+ if not H.has_edge(*e):
+ R.add_edge(*e)
+ return R
+
+
+@nx._dispatchable(graphs=_G_H, returns_graph=True)
+def symmetric_difference(G, H):
+ """Returns new graph with edges that exist in either G or H but not both.
+
+ The node sets of H and G must be the same.
+
+ Parameters
+ ----------
+ G,H : graph
+ A NetworkX graph. G and H must have the same node sets.
+
+ Returns
+ -------
+ D : A new graph with the same type as G.
+
+ Notes
+ -----
+ Attributes from the graph, nodes, and edges are not copied to the new
+ graph.
+
+ Examples
+ --------
+ >>> G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3)])
+ >>> H = nx.Graph([(0, 1), (1, 2), (0, 3)])
+ >>> R = nx.symmetric_difference(G, H)
+ >>> R.nodes
+ NodeView((0, 1, 2, 3))
+ >>> R.edges
+ EdgeView([(0, 2), (0, 3), (1, 3)])
+ """
+ # create new graph
+ if not G.is_multigraph() == H.is_multigraph():
+ raise nx.NetworkXError("G and H must both be graphs or multigraphs.")
+ R = nx.create_empty_copy(G, with_data=False)
+
+ if set(G) != set(H):
+ raise nx.NetworkXError("Node sets of graphs not equal")
+
+ gnodes = set(G) # set of nodes in G
+ hnodes = set(H) # set of nodes in H
+ nodes = gnodes.symmetric_difference(hnodes)
+ R.add_nodes_from(nodes)
+
+ if G.is_multigraph():
+ edges = G.edges(keys=True)
+ else:
+ edges = G.edges()
+ # we could copy the data here but then this function doesn't
+ # match intersection and difference
+ for e in edges:
+ if not H.has_edge(*e):
+ R.add_edge(*e)
+
+ if H.is_multigraph():
+ edges = H.edges(keys=True)
+ else:
+ edges = H.edges()
+ for e in edges:
+ if not G.has_edge(*e):
+ R.add_edge(*e)
+ return R
+
+
+@nx._dispatchable(graphs=_G_H, preserve_all_attrs=True, returns_graph=True)
+def compose(G, H):
+ """Compose graph G with H by combining nodes and edges into a single graph.
+
+ The node sets and edges sets do not need to be disjoint.
+
+ Composing preserves the attributes of nodes and edges.
+ Attribute values from H take precedent over attribute values from G.
+
+ Parameters
+ ----------
+ G, H : graph
+ A NetworkX graph
+
+ Returns
+ -------
+ C: A new graph with the same type as G
+
+ See Also
+ --------
+ :func:`~networkx.Graph.update`
+ union
+ disjoint_union
+
+ Notes
+ -----
+ It is recommended that G and H be either both directed or both undirected.
+
+ For MultiGraphs, the edges are identified by incident nodes AND edge-key.
+ This can cause surprises (i.e., edge `(1, 2)` may or may not be the same
+ in two graphs) if you use MultiGraph without keeping track of edge keys.
+
+ If combining the attributes of common nodes is not desired, consider union(),
+ which raises an exception for name collisions.
+
+ Examples
+ --------
+ >>> G = nx.Graph([(0, 1), (0, 2)])
+ >>> H = nx.Graph([(0, 1), (1, 2)])
+ >>> R = nx.compose(G, H)
+ >>> R.nodes
+ NodeView((0, 1, 2))
+ >>> R.edges
+ EdgeView([(0, 1), (0, 2), (1, 2)])
+
+ By default, the attributes from `H` take precedent over attributes from `G`.
+ If you prefer another way of combining attributes, you can update them after the compose operation:
+
+ >>> G = nx.Graph([(0, 1, {"weight": 2.0}), (3, 0, {"weight": 100.0})])
+ >>> H = nx.Graph([(0, 1, {"weight": 10.0}), (1, 2, {"weight": -1.0})])
+ >>> nx.set_node_attributes(G, {0: "dark", 1: "light", 3: "black"}, name="color")
+ >>> nx.set_node_attributes(H, {0: "green", 1: "orange", 2: "yellow"}, name="color")
+ >>> GcomposeH = nx.compose(G, H)
+
+ Normally, color attribute values of nodes of GcomposeH come from H. We can workaround this as follows:
+
+ >>> node_data = {
+ ... n: G.nodes[n]["color"] + " " + H.nodes[n]["color"]
+ ... for n in G.nodes & H.nodes
+ ... }
+ >>> nx.set_node_attributes(GcomposeH, node_data, "color")
+ >>> print(GcomposeH.nodes[0]["color"])
+ dark green
+
+ >>> print(GcomposeH.nodes[3]["color"])
+ black
+
+ Similarly, we can update edge attributes after the compose operation in a way we prefer:
+
+ >>> edge_data = {
+ ... e: G.edges[e]["weight"] * H.edges[e]["weight"] for e in G.edges & H.edges
+ ... }
+ >>> nx.set_edge_attributes(GcomposeH, edge_data, "weight")
+ >>> print(GcomposeH.edges[(0, 1)]["weight"])
+ 20.0
+
+ >>> print(GcomposeH.edges[(3, 0)]["weight"])
+ 100.0
+ """
+ return nx.compose_all([G, H])
+
+
+@nx._dispatchable(graphs=_G_H, preserve_all_attrs=True, returns_graph=True)
+def full_join(G, H, rename=(None, None)):
+ """Returns the full join of graphs G and H.
+
+ Full join is the union of G and H in which all edges between
+ G and H are added.
+ The node sets of G and H must be disjoint,
+ otherwise an exception is raised.
+
+ Parameters
+ ----------
+ G, H : graph
+ A NetworkX graph
+
+ rename : tuple , default=(None, None)
+ Node names of G and H can be changed by specifying the tuple
+ rename=('G-','H-') (for example). Node "u" in G is then renamed
+ "G-u" and "v" in H is renamed "H-v".
+
+ Returns
+ -------
+ U : The full join graph with the same type as G.
+
+ Notes
+ -----
+ It is recommended that G and H be either both directed or both undirected.
+
+ If G is directed, then edges from G to H are added as well as from H to G.
+
+ Note that full_join() does not produce parallel edges for MultiGraphs.
+
+ The full join operation of graphs G and H is the same as getting
+ their complement, performing a disjoint union, and finally getting
+ the complement of the resulting graph.
+
+ Graph, edge, and node attributes are propagated from G and H
+ to the union graph. If a graph attribute is present in both
+ G and H the value from H is used.
+
+ Examples
+ --------
+ >>> G = nx.Graph([(0, 1), (0, 2)])
+ >>> H = nx.Graph([(3, 4)])
+ >>> R = nx.full_join(G, H, rename=("G", "H"))
+ >>> R.nodes
+ NodeView(('G0', 'G1', 'G2', 'H3', 'H4'))
+ >>> R.edges
+ EdgeView([('G0', 'G1'), ('G0', 'G2'), ('G0', 'H3'), ('G0', 'H4'), ('G1', 'H3'), ('G1', 'H4'), ('G2', 'H3'), ('G2', 'H4'), ('H3', 'H4')])
+
+ See Also
+ --------
+ union
+ disjoint_union
+ """
+ R = union(G, H, rename)
+
+ def add_prefix(graph, prefix):
+ if prefix is None:
+ return graph
+
+ def label(x):
+ return f"{prefix}{x}"
+
+ return nx.relabel_nodes(graph, label)
+
+ G = add_prefix(G, rename[0])
+ H = add_prefix(H, rename[1])
+
+ for i in G:
+ for j in H:
+ R.add_edge(i, j)
+ if R.is_directed():
+ for i in H:
+ for j in G:
+ R.add_edge(i, j)
+
+ return R
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/product.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/product.py
new file mode 100644
index 00000000..28ca78bf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/product.py
@@ -0,0 +1,633 @@
+"""
+Graph products.
+"""
+
+from itertools import product
+
+import networkx as nx
+from networkx.utils import not_implemented_for
+
+__all__ = [
+ "tensor_product",
+ "cartesian_product",
+ "lexicographic_product",
+ "strong_product",
+ "power",
+ "rooted_product",
+ "corona_product",
+ "modular_product",
+]
+_G_H = {"G": 0, "H": 1}
+
+
+def _dict_product(d1, d2):
+ return {k: (d1.get(k), d2.get(k)) for k in set(d1) | set(d2)}
+
+
+# Generators for producing graph products
+def _node_product(G, H):
+ for u, v in product(G, H):
+ yield ((u, v), _dict_product(G.nodes[u], H.nodes[v]))
+
+
+def _directed_edges_cross_edges(G, H):
+ if not G.is_multigraph() and not H.is_multigraph():
+ for u, v, c in G.edges(data=True):
+ for x, y, d in H.edges(data=True):
+ yield (u, x), (v, y), _dict_product(c, d)
+ if not G.is_multigraph() and H.is_multigraph():
+ for u, v, c in G.edges(data=True):
+ for x, y, k, d in H.edges(data=True, keys=True):
+ yield (u, x), (v, y), k, _dict_product(c, d)
+ if G.is_multigraph() and not H.is_multigraph():
+ for u, v, k, c in G.edges(data=True, keys=True):
+ for x, y, d in H.edges(data=True):
+ yield (u, x), (v, y), k, _dict_product(c, d)
+ if G.is_multigraph() and H.is_multigraph():
+ for u, v, j, c in G.edges(data=True, keys=True):
+ for x, y, k, d in H.edges(data=True, keys=True):
+ yield (u, x), (v, y), (j, k), _dict_product(c, d)
+
+
+def _undirected_edges_cross_edges(G, H):
+ if not G.is_multigraph() and not H.is_multigraph():
+ for u, v, c in G.edges(data=True):
+ for x, y, d in H.edges(data=True):
+ yield (v, x), (u, y), _dict_product(c, d)
+ if not G.is_multigraph() and H.is_multigraph():
+ for u, v, c in G.edges(data=True):
+ for x, y, k, d in H.edges(data=True, keys=True):
+ yield (v, x), (u, y), k, _dict_product(c, d)
+ if G.is_multigraph() and not H.is_multigraph():
+ for u, v, k, c in G.edges(data=True, keys=True):
+ for x, y, d in H.edges(data=True):
+ yield (v, x), (u, y), k, _dict_product(c, d)
+ if G.is_multigraph() and H.is_multigraph():
+ for u, v, j, c in G.edges(data=True, keys=True):
+ for x, y, k, d in H.edges(data=True, keys=True):
+ yield (v, x), (u, y), (j, k), _dict_product(c, d)
+
+
+def _edges_cross_nodes(G, H):
+ if G.is_multigraph():
+ for u, v, k, d in G.edges(data=True, keys=True):
+ for x in H:
+ yield (u, x), (v, x), k, d
+ else:
+ for u, v, d in G.edges(data=True):
+ for x in H:
+ if H.is_multigraph():
+ yield (u, x), (v, x), None, d
+ else:
+ yield (u, x), (v, x), d
+
+
+def _nodes_cross_edges(G, H):
+ if H.is_multigraph():
+ for x in G:
+ for u, v, k, d in H.edges(data=True, keys=True):
+ yield (x, u), (x, v), k, d
+ else:
+ for x in G:
+ for u, v, d in H.edges(data=True):
+ if G.is_multigraph():
+ yield (x, u), (x, v), None, d
+ else:
+ yield (x, u), (x, v), d
+
+
+def _edges_cross_nodes_and_nodes(G, H):
+ if G.is_multigraph():
+ for u, v, k, d in G.edges(data=True, keys=True):
+ for x in H:
+ for y in H:
+ yield (u, x), (v, y), k, d
+ else:
+ for u, v, d in G.edges(data=True):
+ for x in H:
+ for y in H:
+ if H.is_multigraph():
+ yield (u, x), (v, y), None, d
+ else:
+ yield (u, x), (v, y), d
+
+
+def _init_product_graph(G, H):
+ if G.is_directed() != H.is_directed():
+ msg = "G and H must be both directed or both undirected"
+ raise nx.NetworkXError(msg)
+ if G.is_multigraph() or H.is_multigraph():
+ GH = nx.MultiGraph()
+ else:
+ GH = nx.Graph()
+ if G.is_directed():
+ GH = GH.to_directed()
+ return GH
+
+
+@nx._dispatchable(graphs=_G_H, preserve_node_attrs=True, returns_graph=True)
+def tensor_product(G, H):
+ r"""Returns the tensor product of G and H.
+
+ The tensor product $P$ of the graphs $G$ and $H$ has a node set that
+ is the Cartesian product of the node sets, $V(P)=V(G) \times V(H)$.
+ $P$ has an edge $((u,v), (x,y))$ if and only if $(u,x)$ is an edge in $G$
+ and $(v,y)$ is an edge in $H$.
+
+ Tensor product is sometimes also referred to as the categorical product,
+ direct product, cardinal product or conjunction.
+
+
+ Parameters
+ ----------
+ G, H: graphs
+ Networkx graphs.
+
+ Returns
+ -------
+ P: NetworkX graph
+ The tensor product of G and H. P will be a multi-graph if either G
+ or H is a multi-graph, will be a directed if G and H are directed,
+ and undirected if G and H are undirected.
+
+ Raises
+ ------
+ NetworkXError
+ If G and H are not both directed or both undirected.
+
+ Notes
+ -----
+ Node attributes in P are two-tuple of the G and H node attributes.
+ Missing attributes are assigned None.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> H = nx.Graph()
+ >>> G.add_node(0, a1=True)
+ >>> H.add_node("a", a2="Spam")
+ >>> P = nx.tensor_product(G, H)
+ >>> list(P)
+ [(0, 'a')]
+
+ Edge attributes and edge keys (for multigraphs) are also copied to the
+ new product graph
+ """
+ GH = _init_product_graph(G, H)
+ GH.add_nodes_from(_node_product(G, H))
+ GH.add_edges_from(_directed_edges_cross_edges(G, H))
+ if not GH.is_directed():
+ GH.add_edges_from(_undirected_edges_cross_edges(G, H))
+ return GH
+
+
+@nx._dispatchable(graphs=_G_H, preserve_node_attrs=True, returns_graph=True)
+def cartesian_product(G, H):
+ r"""Returns the Cartesian product of G and H.
+
+ The Cartesian product $P$ of the graphs $G$ and $H$ has a node set that
+ is the Cartesian product of the node sets, $V(P)=V(G) \times V(H)$.
+ $P$ has an edge $((u,v),(x,y))$ if and only if either $u$ is equal to $x$
+ and both $v$ and $y$ are adjacent in $H$ or if $v$ is equal to $y$ and
+ both $u$ and $x$ are adjacent in $G$.
+
+ Parameters
+ ----------
+ G, H: graphs
+ Networkx graphs.
+
+ Returns
+ -------
+ P: NetworkX graph
+ The Cartesian product of G and H. P will be a multi-graph if either G
+ or H is a multi-graph. Will be a directed if G and H are directed,
+ and undirected if G and H are undirected.
+
+ Raises
+ ------
+ NetworkXError
+ If G and H are not both directed or both undirected.
+
+ Notes
+ -----
+ Node attributes in P are two-tuple of the G and H node attributes.
+ Missing attributes are assigned None.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> H = nx.Graph()
+ >>> G.add_node(0, a1=True)
+ >>> H.add_node("a", a2="Spam")
+ >>> P = nx.cartesian_product(G, H)
+ >>> list(P)
+ [(0, 'a')]
+
+ Edge attributes and edge keys (for multigraphs) are also copied to the
+ new product graph
+ """
+ GH = _init_product_graph(G, H)
+ GH.add_nodes_from(_node_product(G, H))
+ GH.add_edges_from(_edges_cross_nodes(G, H))
+ GH.add_edges_from(_nodes_cross_edges(G, H))
+ return GH
+
+
+@nx._dispatchable(graphs=_G_H, preserve_node_attrs=True, returns_graph=True)
+def lexicographic_product(G, H):
+ r"""Returns the lexicographic product of G and H.
+
+ The lexicographical product $P$ of the graphs $G$ and $H$ has a node set
+ that is the Cartesian product of the node sets, $V(P)=V(G) \times V(H)$.
+ $P$ has an edge $((u,v), (x,y))$ if and only if $(u,v)$ is an edge in $G$
+ or $u==v$ and $(x,y)$ is an edge in $H$.
+
+ Parameters
+ ----------
+ G, H: graphs
+ Networkx graphs.
+
+ Returns
+ -------
+ P: NetworkX graph
+ The Cartesian product of G and H. P will be a multi-graph if either G
+ or H is a multi-graph. Will be a directed if G and H are directed,
+ and undirected if G and H are undirected.
+
+ Raises
+ ------
+ NetworkXError
+ If G and H are not both directed or both undirected.
+
+ Notes
+ -----
+ Node attributes in P are two-tuple of the G and H node attributes.
+ Missing attributes are assigned None.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> H = nx.Graph()
+ >>> G.add_node(0, a1=True)
+ >>> H.add_node("a", a2="Spam")
+ >>> P = nx.lexicographic_product(G, H)
+ >>> list(P)
+ [(0, 'a')]
+
+ Edge attributes and edge keys (for multigraphs) are also copied to the
+ new product graph
+ """
+ GH = _init_product_graph(G, H)
+ GH.add_nodes_from(_node_product(G, H))
+ # Edges in G regardless of H designation
+ GH.add_edges_from(_edges_cross_nodes_and_nodes(G, H))
+ # For each x in G, only if there is an edge in H
+ GH.add_edges_from(_nodes_cross_edges(G, H))
+ return GH
+
+
+@nx._dispatchable(graphs=_G_H, preserve_node_attrs=True, returns_graph=True)
+def strong_product(G, H):
+ r"""Returns the strong product of G and H.
+
+ The strong product $P$ of the graphs $G$ and $H$ has a node set that
+ is the Cartesian product of the node sets, $V(P)=V(G) \times V(H)$.
+ $P$ has an edge $((u,x), (v,y))$ if any of the following conditions
+ are met:
+
+ - $u=v$ and $(x,y)$ is an edge in $H$
+ - $x=y$ and $(u,v)$ is an edge in $G$
+ - $(u,v)$ is an edge in $G$ and $(x,y)$ is an edge in $H$
+
+ Parameters
+ ----------
+ G, H: graphs
+ Networkx graphs.
+
+ Returns
+ -------
+ P: NetworkX graph
+ The Cartesian product of G and H. P will be a multi-graph if either G
+ or H is a multi-graph. Will be a directed if G and H are directed,
+ and undirected if G and H are undirected.
+
+ Raises
+ ------
+ NetworkXError
+ If G and H are not both directed or both undirected.
+
+ Notes
+ -----
+ Node attributes in P are two-tuple of the G and H node attributes.
+ Missing attributes are assigned None.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> H = nx.Graph()
+ >>> G.add_node(0, a1=True)
+ >>> H.add_node("a", a2="Spam")
+ >>> P = nx.strong_product(G, H)
+ >>> list(P)
+ [(0, 'a')]
+
+ Edge attributes and edge keys (for multigraphs) are also copied to the
+ new product graph
+ """
+ GH = _init_product_graph(G, H)
+ GH.add_nodes_from(_node_product(G, H))
+ GH.add_edges_from(_nodes_cross_edges(G, H))
+ GH.add_edges_from(_edges_cross_nodes(G, H))
+ GH.add_edges_from(_directed_edges_cross_edges(G, H))
+ if not GH.is_directed():
+ GH.add_edges_from(_undirected_edges_cross_edges(G, H))
+ return GH
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(returns_graph=True)
+def power(G, k):
+ """Returns the specified power of a graph.
+
+ The $k$th power of a simple graph $G$, denoted $G^k$, is a
+ graph on the same set of nodes in which two distinct nodes $u$ and
+ $v$ are adjacent in $G^k$ if and only if the shortest path
+ distance between $u$ and $v$ in $G$ is at most $k$.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX simple graph object.
+
+ k : positive integer
+ The power to which to raise the graph `G`.
+
+ Returns
+ -------
+ NetworkX simple graph
+ `G` to the power `k`.
+
+ Raises
+ ------
+ ValueError
+ If the exponent `k` is not positive.
+
+ NetworkXNotImplemented
+ If `G` is not a simple graph.
+
+ Examples
+ --------
+ The number of edges will never decrease when taking successive
+ powers:
+
+ >>> G = nx.path_graph(4)
+ >>> list(nx.power(G, 2).edges)
+ [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]
+ >>> list(nx.power(G, 3).edges)
+ [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
+
+ The `k` th power of a cycle graph on *n* nodes is the complete graph
+ on *n* nodes, if `k` is at least ``n // 2``:
+
+ >>> G = nx.cycle_graph(5)
+ >>> H = nx.complete_graph(5)
+ >>> nx.is_isomorphic(nx.power(G, 2), H)
+ True
+ >>> G = nx.cycle_graph(8)
+ >>> H = nx.complete_graph(8)
+ >>> nx.is_isomorphic(nx.power(G, 4), H)
+ True
+
+ References
+ ----------
+ .. [1] J. A. Bondy, U. S. R. Murty, *Graph Theory*. Springer, 2008.
+
+ Notes
+ -----
+ This definition of "power graph" comes from Exercise 3.1.6 of
+ *Graph Theory* by Bondy and Murty [1]_.
+
+ """
+ if k <= 0:
+ raise ValueError("k must be a positive integer")
+ H = nx.Graph()
+ H.add_nodes_from(G)
+ # update BFS code to ignore self loops.
+ for n in G:
+ seen = {} # level (number of hops) when seen in BFS
+ level = 1 # the current level
+ nextlevel = G[n]
+ while nextlevel:
+ thislevel = nextlevel # advance to next level
+ nextlevel = {} # and start a new list (fringe)
+ for v in thislevel:
+ if v == n: # avoid self loop
+ continue
+ if v not in seen:
+ seen[v] = level # set the level of vertex v
+ nextlevel.update(G[v]) # add neighbors of v
+ if k <= level:
+ break
+ level += 1
+ H.add_edges_from((n, nbr) for nbr in seen)
+ return H
+
+
+@not_implemented_for("multigraph")
+@nx._dispatchable(graphs=_G_H, returns_graph=True)
+def rooted_product(G, H, root):
+ """Return the rooted product of graphs G and H rooted at root in H.
+
+ A new graph is constructed representing the rooted product of
+ the inputted graphs, G and H, with a root in H.
+ A rooted product duplicates H for each nodes in G with the root
+ of H corresponding to the node in G. Nodes are renamed as the direct
+ product of G and H. The result is a subgraph of the cartesian product.
+
+ Parameters
+ ----------
+ G,H : graph
+ A NetworkX graph
+ root : node
+ A node in H
+
+ Returns
+ -------
+ R : The rooted product of G and H with a specified root in H
+
+ Notes
+ -----
+ The nodes of R are the Cartesian Product of the nodes of G and H.
+ The nodes of G and H are not relabeled.
+ """
+ if root not in H:
+ raise nx.NodeNotFound("root must be a vertex in H")
+
+ R = nx.Graph()
+ R.add_nodes_from(product(G, H))
+
+ R.add_edges_from(((e[0], root), (e[1], root)) for e in G.edges())
+ R.add_edges_from(((g, e[0]), (g, e[1])) for g in G for e in H.edges())
+
+ return R
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(graphs=_G_H, returns_graph=True)
+def corona_product(G, H):
+ r"""Returns the Corona product of G and H.
+
+ The corona product of $G$ and $H$ is the graph $C = G \circ H$ obtained by
+ taking one copy of $G$, called the center graph, $|V(G)|$ copies of $H$,
+ called the outer graph, and making the $i$-th vertex of $G$ adjacent to
+ every vertex of the $i$-th copy of $H$, where $1 ≤ i ≤ |V(G)|$.
+
+ Parameters
+ ----------
+ G, H: NetworkX graphs
+ The graphs to take the carona product of.
+ `G` is the center graph and `H` is the outer graph
+
+ Returns
+ -------
+ C: NetworkX graph
+ The Corona product of G and H.
+
+ Raises
+ ------
+ NetworkXError
+ If G and H are not both directed or both undirected.
+
+ Examples
+ --------
+ >>> G = nx.cycle_graph(4)
+ >>> H = nx.path_graph(2)
+ >>> C = nx.corona_product(G, H)
+ >>> list(C)
+ [0, 1, 2, 3, (0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (3, 0), (3, 1)]
+ >>> print(C)
+ Graph with 12 nodes and 16 edges
+
+ References
+ ----------
+ [1] M. Tavakoli, F. Rahbarnia, and A. R. Ashrafi,
+ "Studying the corona product of graphs under some graph invariants,"
+ Transactions on Combinatorics, vol. 3, no. 3, pp. 43–49, Sep. 2014,
+ doi: 10.22108/toc.2014.5542.
+ [2] A. Faraji, "Corona Product in Graph Theory," Ali Faraji, May 11, 2021.
+ https://blog.alifaraji.ir/math/graph-theory/corona-product.html (accessed Dec. 07, 2021).
+ """
+ GH = _init_product_graph(G, H)
+ GH.add_nodes_from(G)
+ GH.add_edges_from(G.edges)
+
+ for G_node in G:
+ # copy nodes of H in GH, call it H_i
+ GH.add_nodes_from((G_node, v) for v in H)
+
+ # copy edges of H_i based on H
+ GH.add_edges_from(
+ ((G_node, e0), (G_node, e1), d) for e0, e1, d in H.edges.data()
+ )
+
+ # creating new edges between H_i and a G's node
+ GH.add_edges_from((G_node, (G_node, H_node)) for H_node in H)
+
+ return GH
+
+
+@nx._dispatchable(
+ graphs=_G_H, preserve_edge_attrs=True, preserve_node_attrs=True, returns_graph=True
+)
+def modular_product(G, H):
+ r"""Returns the Modular product of G and H.
+
+ The modular product of `G` and `H` is the graph $M = G \nabla H$,
+ consisting of the node set $V(M) = V(G) \times V(H)$ that is the Cartesian
+ product of the node sets of `G` and `H`. Further, M contains an edge ((u, v), (x, y)):
+
+ - if u is adjacent to x in `G` and v is adjacent to y in `H`, or
+ - if u is not adjacent to x in `G` and v is not adjacent to y in `H`.
+
+ More formally::
+
+ E(M) = {((u, v), (x, y)) | ((u, x) in E(G) and (v, y) in E(H)) or
+ ((u, x) not in E(G) and (v, y) not in E(H))}
+
+ Parameters
+ ----------
+ G, H: NetworkX graphs
+ The graphs to take the modular product of.
+
+ Returns
+ -------
+ M: NetworkX graph
+ The Modular product of `G` and `H`.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If `G` is not a simple graph.
+
+ Examples
+ --------
+ >>> G = nx.cycle_graph(4)
+ >>> H = nx.path_graph(2)
+ >>> M = nx.modular_product(G, H)
+ >>> list(M)
+ [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (3, 0), (3, 1)]
+ >>> print(M)
+ Graph with 8 nodes and 8 edges
+
+ Notes
+ -----
+ The *modular product* is defined in [1]_ and was first
+ introduced as the *weak modular product*.
+
+ The modular product reduces the problem of counting isomorphic subgraphs
+ in `G` and `H` to the problem of counting cliques in M. The subgraphs of
+ `G` and `H` that are induced by the nodes of a clique in M are
+ isomorphic [2]_ [3]_.
+
+ References
+ ----------
+ .. [1] R. Hammack, W. Imrich, and S. Klavžar,
+ "Handbook of Product Graphs", CRC Press, 2011.
+
+ .. [2] H. G. Barrow and R. M. Burstall,
+ "Subgraph isomorphism, matching relational structures and maximal
+ cliques", Information Processing Letters, vol. 4, issue 4, pp. 83-84,
+ 1976, https://doi.org/10.1016/0020-0190(76)90049-1.
+
+ .. [3] V. G. Vizing, "Reduction of the problem of isomorphism and isomorphic
+ entrance to the task of finding the nondensity of a graph." Proc. Third
+ All-Union Conference on Problems of Theoretical Cybernetics. 1974.
+ """
+ if G.is_directed() or H.is_directed():
+ raise nx.NetworkXNotImplemented(
+ "Modular product not implemented for directed graphs"
+ )
+ if G.is_multigraph() or H.is_multigraph():
+ raise nx.NetworkXNotImplemented(
+ "Modular product not implemented for multigraphs"
+ )
+
+ GH = _init_product_graph(G, H)
+ GH.add_nodes_from(_node_product(G, H))
+
+ for u, v, c in G.edges(data=True):
+ for x, y, d in H.edges(data=True):
+ GH.add_edge((u, x), (v, y), **_dict_product(c, d))
+ GH.add_edge((v, x), (u, y), **_dict_product(c, d))
+
+ G = nx.complement(G)
+ H = nx.complement(H)
+
+ for u, v, c in G.edges(data=True):
+ for x, y, d in H.edges(data=True):
+ GH.add_edge((u, x), (v, y), **_dict_product(c, d))
+ GH.add_edge((v, x), (u, y), **_dict_product(c, d))
+
+ return GH
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_all.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_all.py
new file mode 100644
index 00000000..8ec29c15
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_all.py
@@ -0,0 +1,328 @@
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal
+
+
+def test_union_all_attributes():
+ g = nx.Graph()
+ g.add_node(0, x=4)
+ g.add_node(1, x=5)
+ g.add_edge(0, 1, size=5)
+ g.graph["name"] = "g"
+
+ h = g.copy()
+ h.graph["name"] = "h"
+ h.graph["attr"] = "attr"
+ h.nodes[0]["x"] = 7
+
+ j = g.copy()
+ j.graph["name"] = "j"
+ j.graph["attr"] = "attr"
+ j.nodes[0]["x"] = 7
+
+ ghj = nx.union_all([g, h, j], rename=("g", "h", "j"))
+ assert set(ghj.nodes()) == {"h0", "h1", "g0", "g1", "j0", "j1"}
+ for n in ghj:
+ graph, node = n
+ assert ghj.nodes[n] == eval(graph).nodes[int(node)]
+
+ assert ghj.graph["attr"] == "attr"
+ assert ghj.graph["name"] == "j" # j graph attributes take precedent
+
+
+def test_intersection_all():
+ G = nx.Graph()
+ H = nx.Graph()
+ R = nx.Graph(awesome=True)
+ G.add_nodes_from([1, 2, 3, 4])
+ G.add_edge(1, 2)
+ G.add_edge(2, 3)
+ H.add_nodes_from([1, 2, 3, 4])
+ H.add_edge(2, 3)
+ H.add_edge(3, 4)
+ R.add_nodes_from([1, 2, 3, 4])
+ R.add_edge(2, 3)
+ R.add_edge(4, 1)
+ I = nx.intersection_all([G, H, R])
+ assert set(I.nodes()) == {1, 2, 3, 4}
+ assert sorted(I.edges()) == [(2, 3)]
+ assert I.graph == {}
+
+
+def test_intersection_all_different_node_sets():
+ G = nx.Graph()
+ H = nx.Graph()
+ R = nx.Graph()
+ G.add_nodes_from([1, 2, 3, 4, 6, 7])
+ G.add_edge(1, 2)
+ G.add_edge(2, 3)
+ G.add_edge(6, 7)
+ H.add_nodes_from([1, 2, 3, 4])
+ H.add_edge(2, 3)
+ H.add_edge(3, 4)
+ R.add_nodes_from([1, 2, 3, 4, 8, 9])
+ R.add_edge(2, 3)
+ R.add_edge(4, 1)
+ R.add_edge(8, 9)
+ I = nx.intersection_all([G, H, R])
+ assert set(I.nodes()) == {1, 2, 3, 4}
+ assert sorted(I.edges()) == [(2, 3)]
+
+
+def test_intersection_all_attributes():
+ g = nx.Graph()
+ g.add_node(0, x=4)
+ g.add_node(1, x=5)
+ g.add_edge(0, 1, size=5)
+ g.graph["name"] = "g"
+
+ h = g.copy()
+ h.graph["name"] = "h"
+ h.graph["attr"] = "attr"
+ h.nodes[0]["x"] = 7
+
+ gh = nx.intersection_all([g, h])
+ assert set(gh.nodes()) == set(g.nodes())
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == sorted(g.edges())
+
+
+def test_intersection_all_attributes_different_node_sets():
+ g = nx.Graph()
+ g.add_node(0, x=4)
+ g.add_node(1, x=5)
+ g.add_edge(0, 1, size=5)
+ g.graph["name"] = "g"
+
+ h = g.copy()
+ g.add_node(2)
+ h.graph["name"] = "h"
+ h.graph["attr"] = "attr"
+ h.nodes[0]["x"] = 7
+
+ gh = nx.intersection_all([g, h])
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == sorted(g.edges())
+
+
+def test_intersection_all_multigraph_attributes():
+ g = nx.MultiGraph()
+ g.add_edge(0, 1, key=0)
+ g.add_edge(0, 1, key=1)
+ g.add_edge(0, 1, key=2)
+ h = nx.MultiGraph()
+ h.add_edge(0, 1, key=0)
+ h.add_edge(0, 1, key=3)
+ gh = nx.intersection_all([g, h])
+ assert set(gh.nodes()) == set(g.nodes())
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == [(0, 1)]
+ assert sorted(gh.edges(keys=True)) == [(0, 1, 0)]
+
+
+def test_intersection_all_multigraph_attributes_different_node_sets():
+ g = nx.MultiGraph()
+ g.add_edge(0, 1, key=0)
+ g.add_edge(0, 1, key=1)
+ g.add_edge(0, 1, key=2)
+ g.add_edge(1, 2, key=1)
+ g.add_edge(1, 2, key=2)
+ h = nx.MultiGraph()
+ h.add_edge(0, 1, key=0)
+ h.add_edge(0, 1, key=2)
+ h.add_edge(0, 1, key=3)
+ gh = nx.intersection_all([g, h])
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == [(0, 1), (0, 1)]
+ assert sorted(gh.edges(keys=True)) == [(0, 1, 0), (0, 1, 2)]
+
+
+def test_intersection_all_digraph():
+ g = nx.DiGraph()
+ g.add_edges_from([(1, 2), (2, 3)])
+ h = nx.DiGraph()
+ h.add_edges_from([(2, 1), (2, 3)])
+ gh = nx.intersection_all([g, h])
+ assert sorted(gh.edges()) == [(2, 3)]
+
+
+def test_union_all_and_compose_all():
+ K3 = nx.complete_graph(3)
+ P3 = nx.path_graph(3)
+
+ G1 = nx.DiGraph()
+ G1.add_edge("A", "B")
+ G1.add_edge("A", "C")
+ G1.add_edge("A", "D")
+ G2 = nx.DiGraph()
+ G2.add_edge("1", "2")
+ G2.add_edge("1", "3")
+ G2.add_edge("1", "4")
+
+ G = nx.union_all([G1, G2])
+ H = nx.compose_all([G1, G2])
+ assert edges_equal(G.edges(), H.edges())
+ assert not G.has_edge("A", "1")
+ pytest.raises(nx.NetworkXError, nx.union, K3, P3)
+ H1 = nx.union_all([H, G1], rename=("H", "G1"))
+ assert sorted(H1.nodes()) == [
+ "G1A",
+ "G1B",
+ "G1C",
+ "G1D",
+ "H1",
+ "H2",
+ "H3",
+ "H4",
+ "HA",
+ "HB",
+ "HC",
+ "HD",
+ ]
+
+ H2 = nx.union_all([H, G2], rename=("H", ""))
+ assert sorted(H2.nodes()) == [
+ "1",
+ "2",
+ "3",
+ "4",
+ "H1",
+ "H2",
+ "H3",
+ "H4",
+ "HA",
+ "HB",
+ "HC",
+ "HD",
+ ]
+
+ assert not H1.has_edge("NB", "NA")
+
+ G = nx.compose_all([G, G])
+ assert edges_equal(G.edges(), H.edges())
+
+ G2 = nx.union_all([G2, G2], rename=("", "copy"))
+ assert sorted(G2.nodes()) == [
+ "1",
+ "2",
+ "3",
+ "4",
+ "copy1",
+ "copy2",
+ "copy3",
+ "copy4",
+ ]
+
+ assert sorted(G2.neighbors("copy4")) == []
+ assert sorted(G2.neighbors("copy1")) == ["copy2", "copy3", "copy4"]
+ assert len(G) == 8
+ assert nx.number_of_edges(G) == 6
+
+ E = nx.disjoint_union_all([G, G])
+ assert len(E) == 16
+ assert nx.number_of_edges(E) == 12
+
+ E = nx.disjoint_union_all([G1, G2])
+ assert sorted(E.nodes()) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+
+ G1 = nx.DiGraph()
+ G1.add_edge("A", "B")
+ G2 = nx.DiGraph()
+ G2.add_edge(1, 2)
+ G3 = nx.DiGraph()
+ G3.add_edge(11, 22)
+ G4 = nx.union_all([G1, G2, G3], rename=("G1", "G2", "G3"))
+ assert sorted(G4.nodes()) == ["G1A", "G1B", "G21", "G22", "G311", "G322"]
+
+
+def test_union_all_multigraph():
+ G = nx.MultiGraph()
+ G.add_edge(1, 2, key=0)
+ G.add_edge(1, 2, key=1)
+ H = nx.MultiGraph()
+ H.add_edge(3, 4, key=0)
+ H.add_edge(3, 4, key=1)
+ GH = nx.union_all([G, H])
+ assert set(GH) == set(G) | set(H)
+ assert set(GH.edges(keys=True)) == set(G.edges(keys=True)) | set(H.edges(keys=True))
+
+
+def test_input_output():
+ l = [nx.Graph([(1, 2)]), nx.Graph([(3, 4)], awesome=True)]
+ U = nx.disjoint_union_all(l)
+ assert len(l) == 2
+ assert U.graph["awesome"]
+ C = nx.compose_all(l)
+ assert len(l) == 2
+ l = [nx.Graph([(1, 2)]), nx.Graph([(1, 2)])]
+ R = nx.intersection_all(l)
+ assert len(l) == 2
+
+
+def test_mixed_type_union():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.Graph()
+ H = nx.MultiGraph()
+ I = nx.Graph()
+ U = nx.union_all([G, H, I])
+ with pytest.raises(nx.NetworkXError):
+ X = nx.Graph()
+ Y = nx.DiGraph()
+ XY = nx.union_all([X, Y])
+
+
+def test_mixed_type_disjoint_union():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.Graph()
+ H = nx.MultiGraph()
+ I = nx.Graph()
+ U = nx.disjoint_union_all([G, H, I])
+ with pytest.raises(nx.NetworkXError):
+ X = nx.Graph()
+ Y = nx.DiGraph()
+ XY = nx.disjoint_union_all([X, Y])
+
+
+def test_mixed_type_intersection():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.Graph()
+ H = nx.MultiGraph()
+ I = nx.Graph()
+ U = nx.intersection_all([G, H, I])
+ with pytest.raises(nx.NetworkXError):
+ X = nx.Graph()
+ Y = nx.DiGraph()
+ XY = nx.intersection_all([X, Y])
+
+
+def test_mixed_type_compose():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.Graph()
+ H = nx.MultiGraph()
+ I = nx.Graph()
+ U = nx.compose_all([G, H, I])
+ with pytest.raises(nx.NetworkXError):
+ X = nx.Graph()
+ Y = nx.DiGraph()
+ XY = nx.compose_all([X, Y])
+
+
+def test_empty_union():
+ with pytest.raises(ValueError):
+ nx.union_all([])
+
+
+def test_empty_disjoint_union():
+ with pytest.raises(ValueError):
+ nx.disjoint_union_all([])
+
+
+def test_empty_compose_all():
+ with pytest.raises(ValueError):
+ nx.compose_all([])
+
+
+def test_empty_intersection_all():
+ with pytest.raises(ValueError):
+ nx.intersection_all([])
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_binary.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_binary.py
new file mode 100644
index 00000000..c907cd6f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_binary.py
@@ -0,0 +1,453 @@
+import os
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal
+
+
+def test_union_attributes():
+ g = nx.Graph()
+ g.add_node(0, x=4)
+ g.add_node(1, x=5)
+ g.add_edge(0, 1, size=5)
+ g.graph["name"] = "g"
+
+ h = g.copy()
+ h.graph["name"] = "h"
+ h.graph["attr"] = "attr"
+ h.nodes[0]["x"] = 7
+
+ gh = nx.union(g, h, rename=("g", "h"))
+ assert set(gh.nodes()) == {"h0", "h1", "g0", "g1"}
+ for n in gh:
+ graph, node = n
+ assert gh.nodes[n] == eval(graph).nodes[int(node)]
+
+ assert gh.graph["attr"] == "attr"
+ assert gh.graph["name"] == "h" # h graph attributes take precedent
+
+
+def test_intersection():
+ G = nx.Graph()
+ H = nx.Graph()
+ G.add_nodes_from([1, 2, 3, 4])
+ G.add_edge(1, 2)
+ G.add_edge(2, 3)
+ H.add_nodes_from([1, 2, 3, 4])
+ H.add_edge(2, 3)
+ H.add_edge(3, 4)
+ I = nx.intersection(G, H)
+ assert set(I.nodes()) == {1, 2, 3, 4}
+ assert sorted(I.edges()) == [(2, 3)]
+
+
+def test_intersection_node_sets_different():
+ G = nx.Graph()
+ H = nx.Graph()
+ G.add_nodes_from([1, 2, 3, 4, 7])
+ G.add_edge(1, 2)
+ G.add_edge(2, 3)
+ H.add_nodes_from([1, 2, 3, 4, 5, 6])
+ H.add_edge(2, 3)
+ H.add_edge(3, 4)
+ H.add_edge(5, 6)
+ I = nx.intersection(G, H)
+ assert set(I.nodes()) == {1, 2, 3, 4}
+ assert sorted(I.edges()) == [(2, 3)]
+
+
+def test_intersection_attributes():
+ g = nx.Graph()
+ g.add_node(0, x=4)
+ g.add_node(1, x=5)
+ g.add_edge(0, 1, size=5)
+ g.graph["name"] = "g"
+
+ h = g.copy()
+ h.graph["name"] = "h"
+ h.graph["attr"] = "attr"
+ h.nodes[0]["x"] = 7
+ gh = nx.intersection(g, h)
+
+ assert set(gh.nodes()) == set(g.nodes())
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == sorted(g.edges())
+
+
+def test_intersection_attributes_node_sets_different():
+ g = nx.Graph()
+ g.add_node(0, x=4)
+ g.add_node(1, x=5)
+ g.add_node(2, x=3)
+ g.add_edge(0, 1, size=5)
+ g.graph["name"] = "g"
+
+ h = g.copy()
+ h.graph["name"] = "h"
+ h.graph["attr"] = "attr"
+ h.nodes[0]["x"] = 7
+ h.remove_node(2)
+
+ gh = nx.intersection(g, h)
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == sorted(g.edges())
+
+
+def test_intersection_multigraph_attributes():
+ g = nx.MultiGraph()
+ g.add_edge(0, 1, key=0)
+ g.add_edge(0, 1, key=1)
+ g.add_edge(0, 1, key=2)
+ h = nx.MultiGraph()
+ h.add_edge(0, 1, key=0)
+ h.add_edge(0, 1, key=3)
+ gh = nx.intersection(g, h)
+ assert set(gh.nodes()) == set(g.nodes())
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == [(0, 1)]
+ assert sorted(gh.edges(keys=True)) == [(0, 1, 0)]
+
+
+def test_intersection_multigraph_attributes_node_set_different():
+ g = nx.MultiGraph()
+ g.add_edge(0, 1, key=0)
+ g.add_edge(0, 1, key=1)
+ g.add_edge(0, 1, key=2)
+ g.add_edge(0, 2, key=2)
+ g.add_edge(0, 2, key=1)
+ h = nx.MultiGraph()
+ h.add_edge(0, 1, key=0)
+ h.add_edge(0, 1, key=3)
+ gh = nx.intersection(g, h)
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == [(0, 1)]
+ assert sorted(gh.edges(keys=True)) == [(0, 1, 0)]
+
+
+def test_difference():
+ G = nx.Graph()
+ H = nx.Graph()
+ G.add_nodes_from([1, 2, 3, 4])
+ G.add_edge(1, 2)
+ G.add_edge(2, 3)
+ H.add_nodes_from([1, 2, 3, 4])
+ H.add_edge(2, 3)
+ H.add_edge(3, 4)
+ D = nx.difference(G, H)
+ assert set(D.nodes()) == {1, 2, 3, 4}
+ assert sorted(D.edges()) == [(1, 2)]
+ D = nx.difference(H, G)
+ assert set(D.nodes()) == {1, 2, 3, 4}
+ assert sorted(D.edges()) == [(3, 4)]
+ D = nx.symmetric_difference(G, H)
+ assert set(D.nodes()) == {1, 2, 3, 4}
+ assert sorted(D.edges()) == [(1, 2), (3, 4)]
+
+
+def test_difference2():
+ G = nx.Graph()
+ H = nx.Graph()
+ G.add_nodes_from([1, 2, 3, 4])
+ H.add_nodes_from([1, 2, 3, 4])
+ G.add_edge(1, 2)
+ H.add_edge(1, 2)
+ G.add_edge(2, 3)
+ D = nx.difference(G, H)
+ assert set(D.nodes()) == {1, 2, 3, 4}
+ assert sorted(D.edges()) == [(2, 3)]
+ D = nx.difference(H, G)
+ assert set(D.nodes()) == {1, 2, 3, 4}
+ assert sorted(D.edges()) == []
+ H.add_edge(3, 4)
+ D = nx.difference(H, G)
+ assert set(D.nodes()) == {1, 2, 3, 4}
+ assert sorted(D.edges()) == [(3, 4)]
+
+
+def test_difference_attributes():
+ g = nx.Graph()
+ g.add_node(0, x=4)
+ g.add_node(1, x=5)
+ g.add_edge(0, 1, size=5)
+ g.graph["name"] = "g"
+
+ h = g.copy()
+ h.graph["name"] = "h"
+ h.graph["attr"] = "attr"
+ h.nodes[0]["x"] = 7
+
+ gh = nx.difference(g, h)
+ assert set(gh.nodes()) == set(g.nodes())
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == []
+ # node and graph data should not be copied over
+ assert gh.nodes.data() != g.nodes.data()
+ assert gh.graph != g.graph
+
+
+def test_difference_multigraph_attributes():
+ g = nx.MultiGraph()
+ g.add_edge(0, 1, key=0)
+ g.add_edge(0, 1, key=1)
+ g.add_edge(0, 1, key=2)
+ h = nx.MultiGraph()
+ h.add_edge(0, 1, key=0)
+ h.add_edge(0, 1, key=3)
+ gh = nx.difference(g, h)
+ assert set(gh.nodes()) == set(g.nodes())
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == [(0, 1), (0, 1)]
+ assert sorted(gh.edges(keys=True)) == [(0, 1, 1), (0, 1, 2)]
+
+
+def test_difference_raise():
+ G = nx.path_graph(4)
+ H = nx.path_graph(3)
+ pytest.raises(nx.NetworkXError, nx.difference, G, H)
+ pytest.raises(nx.NetworkXError, nx.symmetric_difference, G, H)
+
+
+def test_symmetric_difference_multigraph():
+ g = nx.MultiGraph()
+ g.add_edge(0, 1, key=0)
+ g.add_edge(0, 1, key=1)
+ g.add_edge(0, 1, key=2)
+ h = nx.MultiGraph()
+ h.add_edge(0, 1, key=0)
+ h.add_edge(0, 1, key=3)
+ gh = nx.symmetric_difference(g, h)
+ assert set(gh.nodes()) == set(g.nodes())
+ assert set(gh.nodes()) == set(h.nodes())
+ assert sorted(gh.edges()) == 3 * [(0, 1)]
+ assert sorted(sorted(e) for e in gh.edges(keys=True)) == [
+ [0, 1, 1],
+ [0, 1, 2],
+ [0, 1, 3],
+ ]
+
+
+def test_union_and_compose():
+ K3 = nx.complete_graph(3)
+ P3 = nx.path_graph(3)
+
+ G1 = nx.DiGraph()
+ G1.add_edge("A", "B")
+ G1.add_edge("A", "C")
+ G1.add_edge("A", "D")
+ G2 = nx.DiGraph()
+ G2.add_edge("1", "2")
+ G2.add_edge("1", "3")
+ G2.add_edge("1", "4")
+
+ G = nx.union(G1, G2)
+ H = nx.compose(G1, G2)
+ assert edges_equal(G.edges(), H.edges())
+ assert not G.has_edge("A", 1)
+ pytest.raises(nx.NetworkXError, nx.union, K3, P3)
+ H1 = nx.union(H, G1, rename=("H", "G1"))
+ assert sorted(H1.nodes()) == [
+ "G1A",
+ "G1B",
+ "G1C",
+ "G1D",
+ "H1",
+ "H2",
+ "H3",
+ "H4",
+ "HA",
+ "HB",
+ "HC",
+ "HD",
+ ]
+
+ H2 = nx.union(H, G2, rename=("H", ""))
+ assert sorted(H2.nodes()) == [
+ "1",
+ "2",
+ "3",
+ "4",
+ "H1",
+ "H2",
+ "H3",
+ "H4",
+ "HA",
+ "HB",
+ "HC",
+ "HD",
+ ]
+
+ assert not H1.has_edge("NB", "NA")
+
+ G = nx.compose(G, G)
+ assert edges_equal(G.edges(), H.edges())
+
+ G2 = nx.union(G2, G2, rename=("", "copy"))
+ assert sorted(G2.nodes()) == [
+ "1",
+ "2",
+ "3",
+ "4",
+ "copy1",
+ "copy2",
+ "copy3",
+ "copy4",
+ ]
+
+ assert sorted(G2.neighbors("copy4")) == []
+ assert sorted(G2.neighbors("copy1")) == ["copy2", "copy3", "copy4"]
+ assert len(G) == 8
+ assert nx.number_of_edges(G) == 6
+
+ E = nx.disjoint_union(G, G)
+ assert len(E) == 16
+ assert nx.number_of_edges(E) == 12
+
+ E = nx.disjoint_union(G1, G2)
+ assert sorted(E.nodes()) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+
+ G = nx.Graph()
+ H = nx.Graph()
+ G.add_nodes_from([(1, {"a1": 1})])
+ H.add_nodes_from([(1, {"b1": 1})])
+ R = nx.compose(G, H)
+ assert R.nodes == {1: {"a1": 1, "b1": 1}}
+
+
+def test_union_multigraph():
+ G = nx.MultiGraph()
+ G.add_edge(1, 2, key=0)
+ G.add_edge(1, 2, key=1)
+ H = nx.MultiGraph()
+ H.add_edge(3, 4, key=0)
+ H.add_edge(3, 4, key=1)
+ GH = nx.union(G, H)
+ assert set(GH) == set(G) | set(H)
+ assert set(GH.edges(keys=True)) == set(G.edges(keys=True)) | set(H.edges(keys=True))
+
+
+def test_disjoint_union_multigraph():
+ G = nx.MultiGraph()
+ G.add_edge(0, 1, key=0)
+ G.add_edge(0, 1, key=1)
+ H = nx.MultiGraph()
+ H.add_edge(2, 3, key=0)
+ H.add_edge(2, 3, key=1)
+ GH = nx.disjoint_union(G, H)
+ assert set(GH) == set(G) | set(H)
+ assert set(GH.edges(keys=True)) == set(G.edges(keys=True)) | set(H.edges(keys=True))
+
+
+def test_compose_multigraph():
+ G = nx.MultiGraph()
+ G.add_edge(1, 2, key=0)
+ G.add_edge(1, 2, key=1)
+ H = nx.MultiGraph()
+ H.add_edge(3, 4, key=0)
+ H.add_edge(3, 4, key=1)
+ GH = nx.compose(G, H)
+ assert set(GH) == set(G) | set(H)
+ assert set(GH.edges(keys=True)) == set(G.edges(keys=True)) | set(H.edges(keys=True))
+ H.add_edge(1, 2, key=2)
+ GH = nx.compose(G, H)
+ assert set(GH) == set(G) | set(H)
+ assert set(GH.edges(keys=True)) == set(G.edges(keys=True)) | set(H.edges(keys=True))
+
+
+def test_full_join_graph():
+ # Simple Graphs
+ G = nx.Graph()
+ G.add_node(0)
+ G.add_edge(1, 2)
+ H = nx.Graph()
+ H.add_edge(3, 4)
+
+ U = nx.full_join(G, H)
+ assert set(U) == set(G) | set(H)
+ assert len(U) == len(G) + len(H)
+ assert len(U.edges()) == len(G.edges()) + len(H.edges()) + len(G) * len(H)
+
+ # Rename
+ U = nx.full_join(G, H, rename=("g", "h"))
+ assert set(U) == {"g0", "g1", "g2", "h3", "h4"}
+ assert len(U) == len(G) + len(H)
+ assert len(U.edges()) == len(G.edges()) + len(H.edges()) + len(G) * len(H)
+
+ # Rename graphs with string-like nodes
+ G = nx.Graph()
+ G.add_node("a")
+ G.add_edge("b", "c")
+ H = nx.Graph()
+ H.add_edge("d", "e")
+
+ U = nx.full_join(G, H, rename=("g", "h"))
+ assert set(U) == {"ga", "gb", "gc", "hd", "he"}
+ assert len(U) == len(G) + len(H)
+ assert len(U.edges()) == len(G.edges()) + len(H.edges()) + len(G) * len(H)
+
+ # DiGraphs
+ G = nx.DiGraph()
+ G.add_node(0)
+ G.add_edge(1, 2)
+ H = nx.DiGraph()
+ H.add_edge(3, 4)
+
+ U = nx.full_join(G, H)
+ assert set(U) == set(G) | set(H)
+ assert len(U) == len(G) + len(H)
+ assert len(U.edges()) == len(G.edges()) + len(H.edges()) + len(G) * len(H) * 2
+
+ # DiGraphs Rename
+ U = nx.full_join(G, H, rename=("g", "h"))
+ assert set(U) == {"g0", "g1", "g2", "h3", "h4"}
+ assert len(U) == len(G) + len(H)
+ assert len(U.edges()) == len(G.edges()) + len(H.edges()) + len(G) * len(H) * 2
+
+
+def test_full_join_multigraph():
+ # MultiGraphs
+ G = nx.MultiGraph()
+ G.add_node(0)
+ G.add_edge(1, 2)
+ H = nx.MultiGraph()
+ H.add_edge(3, 4)
+
+ U = nx.full_join(G, H)
+ assert set(U) == set(G) | set(H)
+ assert len(U) == len(G) + len(H)
+ assert len(U.edges()) == len(G.edges()) + len(H.edges()) + len(G) * len(H)
+
+ # MultiGraphs rename
+ U = nx.full_join(G, H, rename=("g", "h"))
+ assert set(U) == {"g0", "g1", "g2", "h3", "h4"}
+ assert len(U) == len(G) + len(H)
+ assert len(U.edges()) == len(G.edges()) + len(H.edges()) + len(G) * len(H)
+
+ # MultiDiGraphs
+ G = nx.MultiDiGraph()
+ G.add_node(0)
+ G.add_edge(1, 2)
+ H = nx.MultiDiGraph()
+ H.add_edge(3, 4)
+
+ U = nx.full_join(G, H)
+ assert set(U) == set(G) | set(H)
+ assert len(U) == len(G) + len(H)
+ assert len(U.edges()) == len(G.edges()) + len(H.edges()) + len(G) * len(H) * 2
+
+ # MultiDiGraphs rename
+ U = nx.full_join(G, H, rename=("g", "h"))
+ assert set(U) == {"g0", "g1", "g2", "h3", "h4"}
+ assert len(U) == len(G) + len(H)
+ assert len(U.edges()) == len(G.edges()) + len(H.edges()) + len(G) * len(H) * 2
+
+
+def test_mixed_type_union():
+ G = nx.Graph()
+ H = nx.MultiGraph()
+ pytest.raises(nx.NetworkXError, nx.union, G, H)
+ pytest.raises(nx.NetworkXError, nx.disjoint_union, G, H)
+ pytest.raises(nx.NetworkXError, nx.intersection, G, H)
+ pytest.raises(nx.NetworkXError, nx.difference, G, H)
+ pytest.raises(nx.NetworkXError, nx.symmetric_difference, G, H)
+ pytest.raises(nx.NetworkXError, nx.compose, G, H)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_product.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_product.py
new file mode 100644
index 00000000..8ee54b93
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_product.py
@@ -0,0 +1,491 @@
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal
+
+
+def test_tensor_product_raises():
+ with pytest.raises(nx.NetworkXError):
+ P = nx.tensor_product(nx.DiGraph(), nx.Graph())
+
+
+def test_tensor_product_null():
+ null = nx.null_graph()
+ empty10 = nx.empty_graph(10)
+ K3 = nx.complete_graph(3)
+ K10 = nx.complete_graph(10)
+ P3 = nx.path_graph(3)
+ P10 = nx.path_graph(10)
+ # null graph
+ G = nx.tensor_product(null, null)
+ assert nx.is_isomorphic(G, null)
+ # null_graph X anything = null_graph and v.v.
+ G = nx.tensor_product(null, empty10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.tensor_product(null, K3)
+ assert nx.is_isomorphic(G, null)
+ G = nx.tensor_product(null, K10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.tensor_product(null, P3)
+ assert nx.is_isomorphic(G, null)
+ G = nx.tensor_product(null, P10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.tensor_product(empty10, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.tensor_product(K3, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.tensor_product(K10, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.tensor_product(P3, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.tensor_product(P10, null)
+ assert nx.is_isomorphic(G, null)
+
+
+def test_tensor_product_size():
+ P5 = nx.path_graph(5)
+ K3 = nx.complete_graph(3)
+ K5 = nx.complete_graph(5)
+
+ G = nx.tensor_product(P5, K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.tensor_product(K3, K5)
+ assert nx.number_of_nodes(G) == 3 * 5
+
+
+def test_tensor_product_combinations():
+ # basic smoke test, more realistic tests would be useful
+ P5 = nx.path_graph(5)
+ K3 = nx.complete_graph(3)
+ G = nx.tensor_product(P5, K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.tensor_product(P5, nx.MultiGraph(K3))
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.tensor_product(nx.MultiGraph(P5), K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.tensor_product(nx.MultiGraph(P5), nx.MultiGraph(K3))
+ assert nx.number_of_nodes(G) == 5 * 3
+
+ G = nx.tensor_product(nx.DiGraph(P5), nx.DiGraph(K3))
+ assert nx.number_of_nodes(G) == 5 * 3
+
+
+def test_tensor_product_classic_result():
+ K2 = nx.complete_graph(2)
+ G = nx.petersen_graph()
+ G = nx.tensor_product(G, K2)
+ assert nx.is_isomorphic(G, nx.desargues_graph())
+
+ G = nx.cycle_graph(5)
+ G = nx.tensor_product(G, K2)
+ assert nx.is_isomorphic(G, nx.cycle_graph(10))
+
+ G = nx.tetrahedral_graph()
+ G = nx.tensor_product(G, K2)
+ assert nx.is_isomorphic(G, nx.cubical_graph())
+
+
+def test_tensor_product_random():
+ G = nx.erdos_renyi_graph(10, 2 / 10.0)
+ H = nx.erdos_renyi_graph(10, 2 / 10.0)
+ GH = nx.tensor_product(G, H)
+
+ for u_G, u_H in GH.nodes():
+ for v_G, v_H in GH.nodes():
+ if H.has_edge(u_H, v_H) and G.has_edge(u_G, v_G):
+ assert GH.has_edge((u_G, u_H), (v_G, v_H))
+ else:
+ assert not GH.has_edge((u_G, u_H), (v_G, v_H))
+
+
+def test_cartesian_product_multigraph():
+ G = nx.MultiGraph()
+ G.add_edge(1, 2, key=0)
+ G.add_edge(1, 2, key=1)
+ H = nx.MultiGraph()
+ H.add_edge(3, 4, key=0)
+ H.add_edge(3, 4, key=1)
+ GH = nx.cartesian_product(G, H)
+ assert set(GH) == {(1, 3), (2, 3), (2, 4), (1, 4)}
+ assert {(frozenset([u, v]), k) for u, v, k in GH.edges(keys=True)} == {
+ (frozenset([u, v]), k)
+ for u, v, k in [
+ ((1, 3), (2, 3), 0),
+ ((1, 3), (2, 3), 1),
+ ((1, 3), (1, 4), 0),
+ ((1, 3), (1, 4), 1),
+ ((2, 3), (2, 4), 0),
+ ((2, 3), (2, 4), 1),
+ ((2, 4), (1, 4), 0),
+ ((2, 4), (1, 4), 1),
+ ]
+ }
+
+
+def test_cartesian_product_raises():
+ with pytest.raises(nx.NetworkXError):
+ P = nx.cartesian_product(nx.DiGraph(), nx.Graph())
+
+
+def test_cartesian_product_null():
+ null = nx.null_graph()
+ empty10 = nx.empty_graph(10)
+ K3 = nx.complete_graph(3)
+ K10 = nx.complete_graph(10)
+ P3 = nx.path_graph(3)
+ P10 = nx.path_graph(10)
+ # null graph
+ G = nx.cartesian_product(null, null)
+ assert nx.is_isomorphic(G, null)
+ # null_graph X anything = null_graph and v.v.
+ G = nx.cartesian_product(null, empty10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.cartesian_product(null, K3)
+ assert nx.is_isomorphic(G, null)
+ G = nx.cartesian_product(null, K10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.cartesian_product(null, P3)
+ assert nx.is_isomorphic(G, null)
+ G = nx.cartesian_product(null, P10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.cartesian_product(empty10, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.cartesian_product(K3, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.cartesian_product(K10, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.cartesian_product(P3, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.cartesian_product(P10, null)
+ assert nx.is_isomorphic(G, null)
+
+
+def test_cartesian_product_size():
+ # order(GXH)=order(G)*order(H)
+ K5 = nx.complete_graph(5)
+ P5 = nx.path_graph(5)
+ K3 = nx.complete_graph(3)
+ G = nx.cartesian_product(P5, K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ assert nx.number_of_edges(G) == nx.number_of_edges(P5) * nx.number_of_nodes(
+ K3
+ ) + nx.number_of_edges(K3) * nx.number_of_nodes(P5)
+ G = nx.cartesian_product(K3, K5)
+ assert nx.number_of_nodes(G) == 3 * 5
+ assert nx.number_of_edges(G) == nx.number_of_edges(K5) * nx.number_of_nodes(
+ K3
+ ) + nx.number_of_edges(K3) * nx.number_of_nodes(K5)
+
+
+def test_cartesian_product_classic():
+ # test some classic product graphs
+ P2 = nx.path_graph(2)
+ P3 = nx.path_graph(3)
+ # cube = 2-path X 2-path
+ G = nx.cartesian_product(P2, P2)
+ G = nx.cartesian_product(P2, G)
+ assert nx.is_isomorphic(G, nx.cubical_graph())
+
+ # 3x3 grid
+ G = nx.cartesian_product(P3, P3)
+ assert nx.is_isomorphic(G, nx.grid_2d_graph(3, 3))
+
+
+def test_cartesian_product_random():
+ G = nx.erdos_renyi_graph(10, 2 / 10.0)
+ H = nx.erdos_renyi_graph(10, 2 / 10.0)
+ GH = nx.cartesian_product(G, H)
+
+ for u_G, u_H in GH.nodes():
+ for v_G, v_H in GH.nodes():
+ if (u_G == v_G and H.has_edge(u_H, v_H)) or (
+ u_H == v_H and G.has_edge(u_G, v_G)
+ ):
+ assert GH.has_edge((u_G, u_H), (v_G, v_H))
+ else:
+ assert not GH.has_edge((u_G, u_H), (v_G, v_H))
+
+
+def test_lexicographic_product_raises():
+ with pytest.raises(nx.NetworkXError):
+ P = nx.lexicographic_product(nx.DiGraph(), nx.Graph())
+
+
+def test_lexicographic_product_null():
+ null = nx.null_graph()
+ empty10 = nx.empty_graph(10)
+ K3 = nx.complete_graph(3)
+ K10 = nx.complete_graph(10)
+ P3 = nx.path_graph(3)
+ P10 = nx.path_graph(10)
+ # null graph
+ G = nx.lexicographic_product(null, null)
+ assert nx.is_isomorphic(G, null)
+ # null_graph X anything = null_graph and v.v.
+ G = nx.lexicographic_product(null, empty10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.lexicographic_product(null, K3)
+ assert nx.is_isomorphic(G, null)
+ G = nx.lexicographic_product(null, K10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.lexicographic_product(null, P3)
+ assert nx.is_isomorphic(G, null)
+ G = nx.lexicographic_product(null, P10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.lexicographic_product(empty10, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.lexicographic_product(K3, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.lexicographic_product(K10, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.lexicographic_product(P3, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.lexicographic_product(P10, null)
+ assert nx.is_isomorphic(G, null)
+
+
+def test_lexicographic_product_size():
+ K5 = nx.complete_graph(5)
+ P5 = nx.path_graph(5)
+ K3 = nx.complete_graph(3)
+ G = nx.lexicographic_product(P5, K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.lexicographic_product(K3, K5)
+ assert nx.number_of_nodes(G) == 3 * 5
+
+
+def test_lexicographic_product_combinations():
+ P5 = nx.path_graph(5)
+ K3 = nx.complete_graph(3)
+ G = nx.lexicographic_product(P5, K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.lexicographic_product(nx.MultiGraph(P5), K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.lexicographic_product(P5, nx.MultiGraph(K3))
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.lexicographic_product(nx.MultiGraph(P5), nx.MultiGraph(K3))
+ assert nx.number_of_nodes(G) == 5 * 3
+
+ # No classic easily found classic results for lexicographic product
+
+
+def test_lexicographic_product_random():
+ G = nx.erdos_renyi_graph(10, 2 / 10.0)
+ H = nx.erdos_renyi_graph(10, 2 / 10.0)
+ GH = nx.lexicographic_product(G, H)
+
+ for u_G, u_H in GH.nodes():
+ for v_G, v_H in GH.nodes():
+ if G.has_edge(u_G, v_G) or (u_G == v_G and H.has_edge(u_H, v_H)):
+ assert GH.has_edge((u_G, u_H), (v_G, v_H))
+ else:
+ assert not GH.has_edge((u_G, u_H), (v_G, v_H))
+
+
+def test_strong_product_raises():
+ with pytest.raises(nx.NetworkXError):
+ P = nx.strong_product(nx.DiGraph(), nx.Graph())
+
+
+def test_strong_product_null():
+ null = nx.null_graph()
+ empty10 = nx.empty_graph(10)
+ K3 = nx.complete_graph(3)
+ K10 = nx.complete_graph(10)
+ P3 = nx.path_graph(3)
+ P10 = nx.path_graph(10)
+ # null graph
+ G = nx.strong_product(null, null)
+ assert nx.is_isomorphic(G, null)
+ # null_graph X anything = null_graph and v.v.
+ G = nx.strong_product(null, empty10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.strong_product(null, K3)
+ assert nx.is_isomorphic(G, null)
+ G = nx.strong_product(null, K10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.strong_product(null, P3)
+ assert nx.is_isomorphic(G, null)
+ G = nx.strong_product(null, P10)
+ assert nx.is_isomorphic(G, null)
+ G = nx.strong_product(empty10, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.strong_product(K3, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.strong_product(K10, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.strong_product(P3, null)
+ assert nx.is_isomorphic(G, null)
+ G = nx.strong_product(P10, null)
+ assert nx.is_isomorphic(G, null)
+
+
+def test_strong_product_size():
+ K5 = nx.complete_graph(5)
+ P5 = nx.path_graph(5)
+ K3 = nx.complete_graph(3)
+ G = nx.strong_product(P5, K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.strong_product(K3, K5)
+ assert nx.number_of_nodes(G) == 3 * 5
+
+
+def test_strong_product_combinations():
+ P5 = nx.path_graph(5)
+ K3 = nx.complete_graph(3)
+ G = nx.strong_product(P5, K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.strong_product(nx.MultiGraph(P5), K3)
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.strong_product(P5, nx.MultiGraph(K3))
+ assert nx.number_of_nodes(G) == 5 * 3
+ G = nx.strong_product(nx.MultiGraph(P5), nx.MultiGraph(K3))
+ assert nx.number_of_nodes(G) == 5 * 3
+
+ # No classic easily found classic results for strong product
+
+
+def test_strong_product_random():
+ G = nx.erdos_renyi_graph(10, 2 / 10.0)
+ H = nx.erdos_renyi_graph(10, 2 / 10.0)
+ GH = nx.strong_product(G, H)
+
+ for u_G, u_H in GH.nodes():
+ for v_G, v_H in GH.nodes():
+ if (
+ (u_G == v_G and H.has_edge(u_H, v_H))
+ or (u_H == v_H and G.has_edge(u_G, v_G))
+ or (G.has_edge(u_G, v_G) and H.has_edge(u_H, v_H))
+ ):
+ assert GH.has_edge((u_G, u_H), (v_G, v_H))
+ else:
+ assert not GH.has_edge((u_G, u_H), (v_G, v_H))
+
+
+def test_graph_power_raises():
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.power(nx.MultiDiGraph(), 2)
+
+
+def test_graph_power():
+ # wikipedia example for graph power
+ G = nx.cycle_graph(7)
+ G.add_edge(6, 7)
+ G.add_edge(7, 8)
+ G.add_edge(8, 9)
+ G.add_edge(9, 2)
+ H = nx.power(G, 2)
+
+ assert edges_equal(
+ list(H.edges()),
+ [
+ (0, 1),
+ (0, 2),
+ (0, 5),
+ (0, 6),
+ (0, 7),
+ (1, 9),
+ (1, 2),
+ (1, 3),
+ (1, 6),
+ (2, 3),
+ (2, 4),
+ (2, 8),
+ (2, 9),
+ (3, 4),
+ (3, 5),
+ (3, 9),
+ (4, 5),
+ (4, 6),
+ (5, 6),
+ (5, 7),
+ (6, 7),
+ (6, 8),
+ (7, 8),
+ (7, 9),
+ (8, 9),
+ ],
+ )
+
+
+def test_graph_power_negative():
+ with pytest.raises(ValueError):
+ nx.power(nx.Graph(), -1)
+
+
+def test_rooted_product_raises():
+ with pytest.raises(nx.NodeNotFound):
+ nx.rooted_product(nx.Graph(), nx.path_graph(2), 10)
+
+
+def test_rooted_product():
+ G = nx.cycle_graph(5)
+ H = nx.Graph()
+ H.add_edges_from([("a", "b"), ("b", "c"), ("b", "d")])
+ R = nx.rooted_product(G, H, "a")
+ assert len(R) == len(G) * len(H)
+ assert R.size() == G.size() + len(G) * H.size()
+
+
+def test_corona_product():
+ G = nx.cycle_graph(3)
+ H = nx.path_graph(2)
+ C = nx.corona_product(G, H)
+ assert len(C) == (len(G) * len(H)) + len(G)
+ assert C.size() == G.size() + len(G) * H.size() + len(G) * len(H)
+
+
+def test_modular_product():
+ G = nx.path_graph(3)
+ H = nx.path_graph(4)
+ M = nx.modular_product(G, H)
+ assert len(M) == len(G) * len(H)
+
+ assert edges_equal(
+ list(M.edges()),
+ [
+ ((0, 0), (1, 1)),
+ ((0, 0), (2, 2)),
+ ((0, 0), (2, 3)),
+ ((0, 1), (1, 0)),
+ ((0, 1), (1, 2)),
+ ((0, 1), (2, 3)),
+ ((0, 2), (1, 1)),
+ ((0, 2), (1, 3)),
+ ((0, 2), (2, 0)),
+ ((0, 3), (1, 2)),
+ ((0, 3), (2, 0)),
+ ((0, 3), (2, 1)),
+ ((1, 0), (2, 1)),
+ ((1, 1), (2, 0)),
+ ((1, 1), (2, 2)),
+ ((1, 2), (2, 1)),
+ ((1, 2), (2, 3)),
+ ((1, 3), (2, 2)),
+ ],
+ )
+
+
+def test_modular_product_raises():
+ G = nx.Graph([(0, 1), (1, 2), (2, 0)])
+ H = nx.Graph([(0, 1), (1, 2), (2, 0)])
+ DG = nx.DiGraph([(0, 1), (1, 2), (2, 0)])
+ DH = nx.DiGraph([(0, 1), (1, 2), (2, 0)])
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.modular_product(G, DH)
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.modular_product(DG, H)
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.modular_product(DG, DH)
+
+ MG = nx.MultiGraph([(0, 1), (1, 2), (2, 0), (0, 1)])
+ MH = nx.MultiGraph([(0, 1), (1, 2), (2, 0), (0, 1)])
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.modular_product(G, MH)
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.modular_product(MG, H)
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.modular_product(MG, MH)
+ with pytest.raises(nx.NetworkXNotImplemented):
+ # check multigraph with no multiedges
+ nx.modular_product(nx.MultiGraph(G), H)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_unary.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_unary.py
new file mode 100644
index 00000000..d68e55cd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/tests/test_unary.py
@@ -0,0 +1,55 @@
+import pytest
+
+import networkx as nx
+
+
+def test_complement():
+ null = nx.null_graph()
+ empty1 = nx.empty_graph(1)
+ empty10 = nx.empty_graph(10)
+ K3 = nx.complete_graph(3)
+ K5 = nx.complete_graph(5)
+ K10 = nx.complete_graph(10)
+ P2 = nx.path_graph(2)
+ P3 = nx.path_graph(3)
+ P5 = nx.path_graph(5)
+ P10 = nx.path_graph(10)
+ # complement of the complete graph is empty
+
+ G = nx.complement(K3)
+ assert nx.is_isomorphic(G, nx.empty_graph(3))
+ G = nx.complement(K5)
+ assert nx.is_isomorphic(G, nx.empty_graph(5))
+ # for any G, G=complement(complement(G))
+ P3cc = nx.complement(nx.complement(P3))
+ assert nx.is_isomorphic(P3, P3cc)
+ nullcc = nx.complement(nx.complement(null))
+ assert nx.is_isomorphic(null, nullcc)
+ b = nx.bull_graph()
+ bcc = nx.complement(nx.complement(b))
+ assert nx.is_isomorphic(b, bcc)
+
+
+def test_complement_2():
+ G1 = nx.DiGraph()
+ G1.add_edge("A", "B")
+ G1.add_edge("A", "C")
+ G1.add_edge("A", "D")
+ G1C = nx.complement(G1)
+ assert sorted(G1C.edges()) == [
+ ("B", "A"),
+ ("B", "C"),
+ ("B", "D"),
+ ("C", "A"),
+ ("C", "B"),
+ ("C", "D"),
+ ("D", "A"),
+ ("D", "B"),
+ ("D", "C"),
+ ]
+
+
+def test_reverse1():
+ # Other tests for reverse are done by the DiGraph and MultiDigraph.
+ G1 = nx.Graph()
+ pytest.raises(nx.NetworkXError, nx.reverse, G1)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/unary.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/unary.py
new file mode 100644
index 00000000..79e44d1c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/operators/unary.py
@@ -0,0 +1,77 @@
+"""Unary operations on graphs"""
+
+import networkx as nx
+
+__all__ = ["complement", "reverse"]
+
+
+@nx._dispatchable(returns_graph=True)
+def complement(G):
+ """Returns the graph complement of G.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ Returns
+ -------
+ GC : A new graph.
+
+ Notes
+ -----
+ Note that `complement` does not create self-loops and also
+ does not produce parallel edges for MultiGraphs.
+
+ Graph, node, and edge data are not propagated to the new graph.
+
+ Examples
+ --------
+ >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (3, 5)])
+ >>> G_complement = nx.complement(G)
+ >>> G_complement.edges() # This shows the edges of the complemented graph
+ EdgeView([(1, 4), (1, 5), (2, 4), (2, 5), (4, 5)])
+
+ """
+ R = G.__class__()
+ R.add_nodes_from(G)
+ R.add_edges_from(
+ ((n, n2) for n, nbrs in G.adjacency() for n2 in G if n2 not in nbrs if n != n2)
+ )
+ return R
+
+
+@nx._dispatchable(returns_graph=True)
+def reverse(G, copy=True):
+ """Returns the reverse directed graph of G.
+
+ Parameters
+ ----------
+ G : directed graph
+ A NetworkX directed graph
+ copy : bool
+ If True, then a new graph is returned. If False, then the graph is
+ reversed in place.
+
+ Returns
+ -------
+ H : directed graph
+ The reversed G.
+
+ Raises
+ ------
+ NetworkXError
+ If graph is undirected.
+
+ Examples
+ --------
+ >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 4), (3, 5)])
+ >>> G_reversed = nx.reverse(G)
+ >>> G_reversed.edges()
+ OutEdgeView([(2, 1), (3, 1), (3, 2), (4, 3), (5, 3)])
+
+ """
+ if not G.is_directed():
+ raise nx.NetworkXError("Cannot reverse an undirected graph.")
+ else:
+ return G.reverse(copy=copy)