aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite
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/bipartite
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/__init__.py87
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/basic.py322
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/centrality.py290
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/cluster.py278
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/covering.py57
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/edgelist.py360
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/extendability.py105
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/generators.py604
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/matching.py590
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/matrix.py168
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/projection.py526
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/redundancy.py112
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/spectral.py69
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_basic.py125
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_centrality.py192
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_cluster.py84
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_covering.py33
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_edgelist.py240
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_extendability.py334
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_generators.py409
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_matching.py327
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_matrix.py84
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_project.py407
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_redundancy.py35
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_spectral_bipartivity.py80
26 files changed, 5918 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/__init__.py
new file mode 100644
index 00000000..7839db96
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/__init__.py
@@ -0,0 +1,87 @@
+r"""This module provides functions and operations for bipartite
+graphs. Bipartite graphs `B = (U, V, E)` have two node sets `U,V` and edges in
+`E` that only connect nodes from opposite sets. It is common in the literature
+to use an spatial analogy referring to the two node sets as top and bottom nodes.
+
+The bipartite algorithms are not imported into the networkx namespace
+at the top level so the easiest way to use them is with:
+
+>>> from networkx.algorithms import bipartite
+
+NetworkX does not have a custom bipartite graph class but the Graph()
+or DiGraph() classes can be used to represent bipartite graphs. However,
+you have to keep track of which set each node belongs to, and make
+sure that there is no edge between nodes of the same set. The convention used
+in NetworkX is to use a node attribute named `bipartite` with values 0 or 1 to
+identify the sets each node belongs to. This convention is not enforced in
+the source code of bipartite functions, it's only a recommendation.
+
+For example:
+
+>>> B = nx.Graph()
+>>> # Add nodes with the node attribute "bipartite"
+>>> B.add_nodes_from([1, 2, 3, 4], bipartite=0)
+>>> B.add_nodes_from(["a", "b", "c"], bipartite=1)
+>>> # Add edges only between nodes of opposite node sets
+>>> B.add_edges_from([(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c"), (4, "a")])
+
+Many algorithms of the bipartite module of NetworkX require, as an argument, a
+container with all the nodes that belong to one set, in addition to the bipartite
+graph `B`. The functions in the bipartite package do not check that the node set
+is actually correct nor that the input graph is actually bipartite.
+If `B` is connected, you can find the two node sets using a two-coloring
+algorithm:
+
+>>> nx.is_connected(B)
+True
+>>> bottom_nodes, top_nodes = bipartite.sets(B)
+
+However, if the input graph is not connected, there are more than one possible
+colorations. This is the reason why we require the user to pass a container
+with all nodes of one bipartite node set as an argument to most bipartite
+functions. In the face of ambiguity, we refuse the temptation to guess and
+raise an :exc:`AmbiguousSolution <networkx.AmbiguousSolution>`
+Exception if the input graph for
+:func:`bipartite.sets <networkx.algorithms.bipartite.basic.sets>`
+is disconnected.
+
+Using the `bipartite` node attribute, you can easily get the two node sets:
+
+>>> top_nodes = {n for n, d in B.nodes(data=True) if d["bipartite"] == 0}
+>>> bottom_nodes = set(B) - top_nodes
+
+So you can easily use the bipartite algorithms that require, as an argument, a
+container with all nodes that belong to one node set:
+
+>>> print(round(bipartite.density(B, bottom_nodes), 2))
+0.5
+>>> G = bipartite.projected_graph(B, top_nodes)
+
+All bipartite graph generators in NetworkX build bipartite graphs with the
+`bipartite` node attribute. Thus, you can use the same approach:
+
+>>> RB = bipartite.random_graph(5, 7, 0.2)
+>>> RB_top = {n for n, d in RB.nodes(data=True) if d["bipartite"] == 0}
+>>> RB_bottom = set(RB) - RB_top
+>>> list(RB_top)
+[0, 1, 2, 3, 4]
+>>> list(RB_bottom)
+[5, 6, 7, 8, 9, 10, 11]
+
+For other bipartite graph generators see
+:mod:`Generators <networkx.algorithms.bipartite.generators>`.
+
+"""
+
+from networkx.algorithms.bipartite.basic import *
+from networkx.algorithms.bipartite.centrality import *
+from networkx.algorithms.bipartite.cluster import *
+from networkx.algorithms.bipartite.covering import *
+from networkx.algorithms.bipartite.edgelist import *
+from networkx.algorithms.bipartite.matching import *
+from networkx.algorithms.bipartite.matrix import *
+from networkx.algorithms.bipartite.projection import *
+from networkx.algorithms.bipartite.redundancy import *
+from networkx.algorithms.bipartite.spectral import *
+from networkx.algorithms.bipartite.generators import *
+from networkx.algorithms.bipartite.extendability import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/basic.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/basic.py
new file mode 100644
index 00000000..8d9a4d5b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/basic.py
@@ -0,0 +1,322 @@
+"""
+==========================
+Bipartite Graph Algorithms
+==========================
+"""
+
+import networkx as nx
+from networkx.algorithms.components import connected_components
+from networkx.exception import AmbiguousSolution
+
+__all__ = [
+ "is_bipartite",
+ "is_bipartite_node_set",
+ "color",
+ "sets",
+ "density",
+ "degrees",
+]
+
+
+@nx._dispatchable
+def color(G):
+ """Returns a two-coloring of the graph.
+
+ Raises an exception if the graph is not bipartite.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ Returns
+ -------
+ color : dictionary
+ A dictionary keyed by node with a 1 or 0 as data for each node color.
+
+ Raises
+ ------
+ NetworkXError
+ If the graph is not two-colorable.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.path_graph(4)
+ >>> c = bipartite.color(G)
+ >>> print(c)
+ {0: 1, 1: 0, 2: 1, 3: 0}
+
+ You can use this to set a node attribute indicating the bipartite set:
+
+ >>> nx.set_node_attributes(G, c, "bipartite")
+ >>> print(G.nodes[0]["bipartite"])
+ 1
+ >>> print(G.nodes[1]["bipartite"])
+ 0
+ """
+ if G.is_directed():
+ import itertools
+
+ def neighbors(v):
+ return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)])
+
+ else:
+ neighbors = G.neighbors
+
+ color = {}
+ for n in G: # handle disconnected graphs
+ if n in color or len(G[n]) == 0: # skip isolates
+ continue
+ queue = [n]
+ color[n] = 1 # nodes seen with color (1 or 0)
+ while queue:
+ v = queue.pop()
+ c = 1 - color[v] # opposite color of node v
+ for w in neighbors(v):
+ if w in color:
+ if color[w] == color[v]:
+ raise nx.NetworkXError("Graph is not bipartite.")
+ else:
+ color[w] = c
+ queue.append(w)
+ # color isolates with 0
+ color.update(dict.fromkeys(nx.isolates(G), 0))
+ return color
+
+
+@nx._dispatchable
+def is_bipartite(G):
+ """Returns True if graph G is bipartite, False if not.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.path_graph(4)
+ >>> print(bipartite.is_bipartite(G))
+ True
+
+ See Also
+ --------
+ color, is_bipartite_node_set
+ """
+ try:
+ color(G)
+ return True
+ except nx.NetworkXError:
+ return False
+
+
+@nx._dispatchable
+def is_bipartite_node_set(G, nodes):
+ """Returns True if nodes and G/nodes are a bipartition of G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ nodes: list or container
+ Check if nodes are a one of a bipartite set.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.path_graph(4)
+ >>> X = set([1, 3])
+ >>> bipartite.is_bipartite_node_set(G, X)
+ True
+
+ Notes
+ -----
+ An exception is raised if the input nodes are not distinct, because in this
+ case some bipartite algorithms will yield incorrect results.
+ For connected graphs the bipartite sets are unique. This function handles
+ disconnected graphs.
+ """
+ S = set(nodes)
+
+ if len(S) < len(nodes):
+ # this should maybe just return False?
+ raise AmbiguousSolution(
+ "The input node set contains duplicates.\n"
+ "This may lead to incorrect results when using it in bipartite algorithms.\n"
+ "Consider using set(nodes) as the input"
+ )
+
+ for CC in (G.subgraph(c).copy() for c in connected_components(G)):
+ X, Y = sets(CC)
+ if not (
+ (X.issubset(S) and Y.isdisjoint(S)) or (Y.issubset(S) and X.isdisjoint(S))
+ ):
+ return False
+ return True
+
+
+@nx._dispatchable
+def sets(G, top_nodes=None):
+ """Returns bipartite node sets of graph G.
+
+ Raises an exception if the graph is not bipartite or if the input
+ graph is disconnected and thus more than one valid solution exists.
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ top_nodes : container, optional
+ Container with all nodes in one bipartite node set. If not supplied
+ it will be computed. But if more than one solution exists an exception
+ will be raised.
+
+ Returns
+ -------
+ X : set
+ Nodes from one side of the bipartite graph.
+ Y : set
+ Nodes from the other side.
+
+ Raises
+ ------
+ AmbiguousSolution
+ Raised if the input bipartite graph is disconnected and no container
+ with all nodes in one bipartite set is provided. When determining
+ the nodes in each bipartite set more than one valid solution is
+ possible if the input graph is disconnected.
+ NetworkXError
+ Raised if the input graph is not bipartite.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.path_graph(4)
+ >>> X, Y = bipartite.sets(G)
+ >>> list(X)
+ [0, 2]
+ >>> list(Y)
+ [1, 3]
+
+ See Also
+ --------
+ color
+
+ """
+ if G.is_directed():
+ is_connected = nx.is_weakly_connected
+ else:
+ is_connected = nx.is_connected
+ if top_nodes is not None:
+ X = set(top_nodes)
+ Y = set(G) - X
+ else:
+ if not is_connected(G):
+ msg = "Disconnected graph: Ambiguous solution for bipartite sets."
+ raise nx.AmbiguousSolution(msg)
+ c = color(G)
+ X = {n for n, is_top in c.items() if is_top}
+ Y = {n for n, is_top in c.items() if not is_top}
+ return (X, Y)
+
+
+@nx._dispatchable(graphs="B")
+def density(B, nodes):
+ """Returns density of bipartite graph B.
+
+ Parameters
+ ----------
+ B : NetworkX graph
+
+ nodes: list or container
+ Nodes in one node set of the bipartite graph.
+
+ Returns
+ -------
+ d : float
+ The bipartite density
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.complete_bipartite_graph(3, 2)
+ >>> X = set([0, 1, 2])
+ >>> bipartite.density(G, X)
+ 1.0
+ >>> Y = set([3, 4])
+ >>> bipartite.density(G, Y)
+ 1.0
+
+ Notes
+ -----
+ The container of nodes passed as argument must contain all nodes
+ in one of the two bipartite node sets to avoid ambiguity in the
+ case of disconnected graphs.
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ See Also
+ --------
+ color
+ """
+ n = len(B)
+ m = nx.number_of_edges(B)
+ nb = len(nodes)
+ nt = n - nb
+ if m == 0: # includes cases n==0 and n==1
+ d = 0.0
+ else:
+ if B.is_directed():
+ d = m / (2 * nb * nt)
+ else:
+ d = m / (nb * nt)
+ return d
+
+
+@nx._dispatchable(graphs="B", edge_attrs="weight")
+def degrees(B, nodes, weight=None):
+ """Returns the degrees of the two node sets in the bipartite graph B.
+
+ Parameters
+ ----------
+ B : NetworkX graph
+
+ nodes: list or container
+ Nodes in one node set of the bipartite graph.
+
+ weight : string or None, optional (default=None)
+ The edge attribute that holds the numerical value used as a weight.
+ If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ Returns
+ -------
+ (degX,degY) : tuple of dictionaries
+ The degrees of the two bipartite sets as dictionaries keyed by node.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.complete_bipartite_graph(3, 2)
+ >>> Y = set([3, 4])
+ >>> degX, degY = bipartite.degrees(G, Y)
+ >>> dict(degX)
+ {0: 2, 1: 2, 2: 2}
+
+ Notes
+ -----
+ The container of nodes passed as argument must contain all nodes
+ in one of the two bipartite node sets to avoid ambiguity in the
+ case of disconnected graphs.
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ See Also
+ --------
+ color, density
+ """
+ bottom = set(nodes)
+ top = set(B) - bottom
+ return (B.degree(top, weight), B.degree(bottom, weight))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/centrality.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/centrality.py
new file mode 100644
index 00000000..42d7270e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/centrality.py
@@ -0,0 +1,290 @@
+import networkx as nx
+
+__all__ = ["degree_centrality", "betweenness_centrality", "closeness_centrality"]
+
+
+@nx._dispatchable(name="bipartite_degree_centrality")
+def degree_centrality(G, nodes):
+ r"""Compute the degree centrality for nodes in a bipartite network.
+
+ The degree centrality for a node `v` is the fraction of nodes
+ connected to it.
+
+ Parameters
+ ----------
+ G : graph
+ A bipartite network
+
+ nodes : list or container
+ Container with all nodes in one bipartite node set.
+
+ Returns
+ -------
+ centrality : dictionary
+ Dictionary keyed by node with bipartite degree centrality as the value.
+
+ Examples
+ --------
+ >>> G = nx.wheel_graph(5)
+ >>> top_nodes = {0, 1, 2}
+ >>> nx.bipartite.degree_centrality(G, nodes=top_nodes)
+ {0: 2.0, 1: 1.5, 2: 1.5, 3: 1.0, 4: 1.0}
+
+ See Also
+ --------
+ betweenness_centrality
+ closeness_centrality
+ :func:`~networkx.algorithms.bipartite.basic.sets`
+ :func:`~networkx.algorithms.bipartite.basic.is_bipartite`
+
+ Notes
+ -----
+ The nodes input parameter must contain all nodes in one bipartite node set,
+ but the dictionary returned contains all nodes from both bipartite node
+ sets. See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ For unipartite networks, the degree centrality values are
+ normalized by dividing by the maximum possible degree (which is
+ `n-1` where `n` is the number of nodes in G).
+
+ In the bipartite case, the maximum possible degree of a node in a
+ bipartite node set is the number of nodes in the opposite node set
+ [1]_. The degree centrality for a node `v` in the bipartite
+ sets `U` with `n` nodes and `V` with `m` nodes is
+
+ .. math::
+
+ d_{v} = \frac{deg(v)}{m}, \mbox{for} v \in U ,
+
+ d_{v} = \frac{deg(v)}{n}, \mbox{for} v \in V ,
+
+
+ where `deg(v)` is the degree of node `v`.
+
+ References
+ ----------
+ .. [1] Borgatti, S.P. and Halgin, D. In press. "Analyzing Affiliation
+ Networks". In Carrington, P. and Scott, J. (eds) The Sage Handbook
+ of Social Network Analysis. Sage Publications.
+ https://dx.doi.org/10.4135/9781446294413.n28
+ """
+ top = set(nodes)
+ bottom = set(G) - top
+ s = 1.0 / len(bottom)
+ centrality = {n: d * s for n, d in G.degree(top)}
+ s = 1.0 / len(top)
+ centrality.update({n: d * s for n, d in G.degree(bottom)})
+ return centrality
+
+
+@nx._dispatchable(name="bipartite_betweenness_centrality")
+def betweenness_centrality(G, nodes):
+ r"""Compute betweenness centrality for nodes in a bipartite network.
+
+ Betweenness centrality of a node `v` is the sum of the
+ fraction of all-pairs shortest paths that pass through `v`.
+
+ Values of betweenness are normalized by the maximum possible
+ value which for bipartite graphs is limited by the relative size
+ of the two node sets [1]_.
+
+ Let `n` be the number of nodes in the node set `U` and
+ `m` be the number of nodes in the node set `V`, then
+ nodes in `U` are normalized by dividing by
+
+ .. math::
+
+ \frac{1}{2} [m^2 (s + 1)^2 + m (s + 1)(2t - s - 1) - t (2s - t + 3)] ,
+
+ where
+
+ .. math::
+
+ s = (n - 1) \div m , t = (n - 1) \mod m ,
+
+ and nodes in `V` are normalized by dividing by
+
+ .. math::
+
+ \frac{1}{2} [n^2 (p + 1)^2 + n (p + 1)(2r - p - 1) - r (2p - r + 3)] ,
+
+ where,
+
+ .. math::
+
+ p = (m - 1) \div n , r = (m - 1) \mod n .
+
+ Parameters
+ ----------
+ G : graph
+ A bipartite graph
+
+ nodes : list or container
+ Container with all nodes in one bipartite node set.
+
+ Returns
+ -------
+ betweenness : dictionary
+ Dictionary keyed by node with bipartite betweenness centrality
+ as the value.
+
+ Examples
+ --------
+ >>> G = nx.cycle_graph(4)
+ >>> top_nodes = {1, 2}
+ >>> nx.bipartite.betweenness_centrality(G, nodes=top_nodes)
+ {0: 0.25, 1: 0.25, 2: 0.25, 3: 0.25}
+
+ See Also
+ --------
+ degree_centrality
+ closeness_centrality
+ :func:`~networkx.algorithms.bipartite.basic.sets`
+ :func:`~networkx.algorithms.bipartite.basic.is_bipartite`
+
+ Notes
+ -----
+ The nodes input parameter must contain all nodes in one bipartite node set,
+ but the dictionary returned contains all nodes from both node sets.
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+
+ References
+ ----------
+ .. [1] Borgatti, S.P. and Halgin, D. In press. "Analyzing Affiliation
+ Networks". In Carrington, P. and Scott, J. (eds) The Sage Handbook
+ of Social Network Analysis. Sage Publications.
+ https://dx.doi.org/10.4135/9781446294413.n28
+ """
+ top = set(nodes)
+ bottom = set(G) - top
+ n = len(top)
+ m = len(bottom)
+ s, t = divmod(n - 1, m)
+ bet_max_top = (
+ ((m**2) * ((s + 1) ** 2))
+ + (m * (s + 1) * (2 * t - s - 1))
+ - (t * ((2 * s) - t + 3))
+ ) / 2.0
+ p, r = divmod(m - 1, n)
+ bet_max_bot = (
+ ((n**2) * ((p + 1) ** 2))
+ + (n * (p + 1) * (2 * r - p - 1))
+ - (r * ((2 * p) - r + 3))
+ ) / 2.0
+ betweenness = nx.betweenness_centrality(G, normalized=False, weight=None)
+ for node in top:
+ betweenness[node] /= bet_max_top
+ for node in bottom:
+ betweenness[node] /= bet_max_bot
+ return betweenness
+
+
+@nx._dispatchable(name="bipartite_closeness_centrality")
+def closeness_centrality(G, nodes, normalized=True):
+ r"""Compute the closeness centrality for nodes in a bipartite network.
+
+ The closeness of a node is the distance to all other nodes in the
+ graph or in the case that the graph is not connected to all other nodes
+ in the connected component containing that node.
+
+ Parameters
+ ----------
+ G : graph
+ A bipartite network
+
+ nodes : list or container
+ Container with all nodes in one bipartite node set.
+
+ normalized : bool, optional
+ If True (default) normalize by connected component size.
+
+ Returns
+ -------
+ closeness : dictionary
+ Dictionary keyed by node with bipartite closeness centrality
+ as the value.
+
+ Examples
+ --------
+ >>> G = nx.wheel_graph(5)
+ >>> top_nodes = {0, 1, 2}
+ >>> nx.bipartite.closeness_centrality(G, nodes=top_nodes)
+ {0: 1.5, 1: 1.2, 2: 1.2, 3: 1.0, 4: 1.0}
+
+ See Also
+ --------
+ betweenness_centrality
+ degree_centrality
+ :func:`~networkx.algorithms.bipartite.basic.sets`
+ :func:`~networkx.algorithms.bipartite.basic.is_bipartite`
+
+ Notes
+ -----
+ The nodes input parameter must contain all nodes in one bipartite node set,
+ but the dictionary returned contains all nodes from both node sets.
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+
+ Closeness centrality is normalized by the minimum distance possible.
+ In the bipartite case the minimum distance for a node in one bipartite
+ node set is 1 from all nodes in the other node set and 2 from all
+ other nodes in its own set [1]_. Thus the closeness centrality
+ for node `v` in the two bipartite sets `U` with
+ `n` nodes and `V` with `m` nodes is
+
+ .. math::
+
+ c_{v} = \frac{m + 2(n - 1)}{d}, \mbox{for} v \in U,
+
+ c_{v} = \frac{n + 2(m - 1)}{d}, \mbox{for} v \in V,
+
+ where `d` is the sum of the distances from `v` to all
+ other nodes.
+
+ Higher values of closeness indicate higher centrality.
+
+ As in the unipartite case, setting normalized=True causes the
+ values to normalized further to n-1 / size(G)-1 where n is the
+ number of nodes in the connected part of graph containing the
+ node. If the graph is not completely connected, this algorithm
+ computes the closeness centrality for each connected part
+ separately.
+
+ References
+ ----------
+ .. [1] Borgatti, S.P. and Halgin, D. In press. "Analyzing Affiliation
+ Networks". In Carrington, P. and Scott, J. (eds) The Sage Handbook
+ of Social Network Analysis. Sage Publications.
+ https://dx.doi.org/10.4135/9781446294413.n28
+ """
+ closeness = {}
+ path_length = nx.single_source_shortest_path_length
+ top = set(nodes)
+ bottom = set(G) - top
+ n = len(top)
+ m = len(bottom)
+ for node in top:
+ sp = dict(path_length(G, node))
+ totsp = sum(sp.values())
+ if totsp > 0.0 and len(G) > 1:
+ closeness[node] = (m + 2 * (n - 1)) / totsp
+ if normalized:
+ s = (len(sp) - 1) / (len(G) - 1)
+ closeness[node] *= s
+ else:
+ closeness[node] = 0.0
+ for node in bottom:
+ sp = dict(path_length(G, node))
+ totsp = sum(sp.values())
+ if totsp > 0.0 and len(G) > 1:
+ closeness[node] = (n + 2 * (m - 1)) / totsp
+ if normalized:
+ s = (len(sp) - 1) / (len(G) - 1)
+ closeness[node] *= s
+ else:
+ closeness[node] = 0.0
+ return closeness
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/cluster.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/cluster.py
new file mode 100644
index 00000000..5b66b280
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/cluster.py
@@ -0,0 +1,278 @@
+"""Functions for computing clustering of pairs"""
+
+import itertools
+
+import networkx as nx
+
+__all__ = [
+ "clustering",
+ "average_clustering",
+ "latapy_clustering",
+ "robins_alexander_clustering",
+]
+
+
+def cc_dot(nu, nv):
+ return len(nu & nv) / len(nu | nv)
+
+
+def cc_max(nu, nv):
+ return len(nu & nv) / max(len(nu), len(nv))
+
+
+def cc_min(nu, nv):
+ return len(nu & nv) / min(len(nu), len(nv))
+
+
+modes = {"dot": cc_dot, "min": cc_min, "max": cc_max}
+
+
+@nx._dispatchable
+def latapy_clustering(G, nodes=None, mode="dot"):
+ r"""Compute a bipartite clustering coefficient for nodes.
+
+ The bipartite clustering coefficient is a measure of local density
+ of connections defined as [1]_:
+
+ .. math::
+
+ c_u = \frac{\sum_{v \in N(N(u))} c_{uv} }{|N(N(u))|}
+
+ where `N(N(u))` are the second order neighbors of `u` in `G` excluding `u`,
+ and `c_{uv}` is the pairwise clustering coefficient between nodes
+ `u` and `v`.
+
+ The mode selects the function for `c_{uv}` which can be:
+
+ `dot`:
+
+ .. math::
+
+ c_{uv}=\frac{|N(u)\cap N(v)|}{|N(u) \cup N(v)|}
+
+ `min`:
+
+ .. math::
+
+ c_{uv}=\frac{|N(u)\cap N(v)|}{min(|N(u)|,|N(v)|)}
+
+ `max`:
+
+ .. math::
+
+ c_{uv}=\frac{|N(u)\cap N(v)|}{max(|N(u)|,|N(v)|)}
+
+
+ Parameters
+ ----------
+ G : graph
+ A bipartite graph
+
+ nodes : list or iterable (optional)
+ Compute bipartite clustering for these nodes. The default
+ is all nodes in G.
+
+ mode : string
+ The pairwise bipartite clustering method to be used in the computation.
+ It must be "dot", "max", or "min".
+
+ Returns
+ -------
+ clustering : dictionary
+ A dictionary keyed by node with the clustering coefficient value.
+
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.path_graph(4) # path graphs are bipartite
+ >>> c = bipartite.clustering(G)
+ >>> c[0]
+ 0.5
+ >>> c = bipartite.clustering(G, mode="min")
+ >>> c[0]
+ 1.0
+
+ See Also
+ --------
+ robins_alexander_clustering
+ average_clustering
+ networkx.algorithms.cluster.square_clustering
+
+ References
+ ----------
+ .. [1] Latapy, Matthieu, Clémence Magnien, and Nathalie Del Vecchio (2008).
+ Basic notions for the analysis of large two-mode networks.
+ Social Networks 30(1), 31--48.
+ """
+ if not nx.algorithms.bipartite.is_bipartite(G):
+ raise nx.NetworkXError("Graph is not bipartite")
+
+ try:
+ cc_func = modes[mode]
+ except KeyError as err:
+ raise nx.NetworkXError(
+ "Mode for bipartite clustering must be: dot, min or max"
+ ) from err
+
+ if nodes is None:
+ nodes = G
+ ccs = {}
+ for v in nodes:
+ cc = 0.0
+ nbrs2 = {u for nbr in G[v] for u in G[nbr]} - {v}
+ for u in nbrs2:
+ cc += cc_func(set(G[u]), set(G[v]))
+ if cc > 0.0: # len(nbrs2)>0
+ cc /= len(nbrs2)
+ ccs[v] = cc
+ return ccs
+
+
+clustering = latapy_clustering
+
+
+@nx._dispatchable(name="bipartite_average_clustering")
+def average_clustering(G, nodes=None, mode="dot"):
+ r"""Compute the average bipartite clustering coefficient.
+
+ A clustering coefficient for the whole graph is the average,
+
+ .. math::
+
+ C = \frac{1}{n}\sum_{v \in G} c_v,
+
+ where `n` is the number of nodes in `G`.
+
+ Similar measures for the two bipartite sets can be defined [1]_
+
+ .. math::
+
+ C_X = \frac{1}{|X|}\sum_{v \in X} c_v,
+
+ where `X` is a bipartite set of `G`.
+
+ Parameters
+ ----------
+ G : graph
+ a bipartite graph
+
+ nodes : list or iterable, optional
+ A container of nodes to use in computing the average.
+ The nodes should be either the entire graph (the default) or one of the
+ bipartite sets.
+
+ mode : string
+ The pairwise bipartite clustering method.
+ It must be "dot", "max", or "min"
+
+ Returns
+ -------
+ clustering : float
+ The average bipartite clustering for the given set of nodes or the
+ entire graph if no nodes are specified.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.star_graph(3) # star graphs are bipartite
+ >>> bipartite.average_clustering(G)
+ 0.75
+ >>> X, Y = bipartite.sets(G)
+ >>> bipartite.average_clustering(G, X)
+ 0.0
+ >>> bipartite.average_clustering(G, Y)
+ 1.0
+
+ See Also
+ --------
+ clustering
+
+ Notes
+ -----
+ The container of nodes passed to this function must contain all of the nodes
+ in one of the bipartite sets ("top" or "bottom") in order to compute
+ the correct average bipartite clustering coefficients.
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+
+ References
+ ----------
+ .. [1] Latapy, Matthieu, Clémence Magnien, and Nathalie Del Vecchio (2008).
+ Basic notions for the analysis of large two-mode networks.
+ Social Networks 30(1), 31--48.
+ """
+ if nodes is None:
+ nodes = G
+ ccs = latapy_clustering(G, nodes=nodes, mode=mode)
+ return sum(ccs[v] for v in nodes) / len(nodes)
+
+
+@nx._dispatchable
+def robins_alexander_clustering(G):
+ r"""Compute the bipartite clustering of G.
+
+ Robins and Alexander [1]_ defined bipartite clustering coefficient as
+ four times the number of four cycles `C_4` divided by the number of
+ three paths `L_3` in a bipartite graph:
+
+ .. math::
+
+ CC_4 = \frac{4 * C_4}{L_3}
+
+ Parameters
+ ----------
+ G : graph
+ a bipartite graph
+
+ Returns
+ -------
+ clustering : float
+ The Robins and Alexander bipartite clustering for the input graph.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.davis_southern_women_graph()
+ >>> print(round(bipartite.robins_alexander_clustering(G), 3))
+ 0.468
+
+ See Also
+ --------
+ latapy_clustering
+ networkx.algorithms.cluster.square_clustering
+
+ References
+ ----------
+ .. [1] Robins, G. and M. Alexander (2004). Small worlds among interlocking
+ directors: Network structure and distance in bipartite graphs.
+ Computational & Mathematical Organization Theory 10(1), 69–94.
+
+ """
+ if G.order() < 4 or G.size() < 3:
+ return 0
+ L_3 = _threepaths(G)
+ if L_3 == 0:
+ return 0
+ C_4 = _four_cycles(G)
+ return (4.0 * C_4) / L_3
+
+
+def _four_cycles(G):
+ cycles = 0
+ for v in G:
+ for u, w in itertools.combinations(G[v], 2):
+ cycles += len((set(G[u]) & set(G[w])) - {v})
+ return cycles / 4
+
+
+def _threepaths(G):
+ paths = 0
+ for v in G:
+ for u in G[v]:
+ for w in set(G[u]) - {v}:
+ paths += len(set(G[w]) - {v, u})
+ # Divide by two because we count each three path twice
+ # one for each possible starting point
+ return paths / 2
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/covering.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/covering.py
new file mode 100644
index 00000000..f937903e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/covering.py
@@ -0,0 +1,57 @@
+"""Functions related to graph covers."""
+
+import networkx as nx
+from networkx.algorithms.bipartite.matching import hopcroft_karp_matching
+from networkx.algorithms.covering import min_edge_cover as _min_edge_cover
+from networkx.utils import not_implemented_for
+
+__all__ = ["min_edge_cover"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(name="bipartite_min_edge_cover")
+def min_edge_cover(G, matching_algorithm=None):
+ """Returns a set of edges which constitutes
+ the minimum edge cover of the graph.
+
+ The smallest edge cover can be found in polynomial time by finding
+ a maximum matching and extending it greedily so that all nodes
+ are covered.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected bipartite graph.
+
+ matching_algorithm : function
+ A function that returns a maximum cardinality matching in a
+ given bipartite graph. The function must take one input, the
+ graph ``G``, and return a dictionary mapping each node to its
+ mate. If not specified,
+ :func:`~networkx.algorithms.bipartite.matching.hopcroft_karp_matching`
+ will be used. Other possibilities include
+ :func:`~networkx.algorithms.bipartite.matching.eppstein_matching`,
+
+ Returns
+ -------
+ set
+ A set of the edges in a minimum edge cover of the graph, given as
+ pairs of nodes. It contains both the edges `(u, v)` and `(v, u)`
+ for given nodes `u` and `v` among the edges of minimum edge cover.
+
+ Notes
+ -----
+ An edge cover of a graph is a set of edges such that every node of
+ the graph is incident to at least one edge of the set.
+ A minimum edge cover is an edge covering of smallest cardinality.
+
+ Due to its implementation, the worst-case running time of this algorithm
+ is bounded by the worst-case running time of the function
+ ``matching_algorithm``.
+ """
+ if G.order() == 0: # Special case for the empty graph
+ return set()
+ if matching_algorithm is None:
+ matching_algorithm = hopcroft_karp_matching
+ return _min_edge_cover(G, matching_algorithm=matching_algorithm)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/edgelist.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/edgelist.py
new file mode 100644
index 00000000..db6ef9d8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/edgelist.py
@@ -0,0 +1,360 @@
+"""
+********************
+Bipartite Edge Lists
+********************
+Read and write NetworkX graphs as bipartite edge lists.
+
+Format
+------
+You can read or write three formats of edge lists with these functions.
+
+Node pairs with no data::
+
+ 1 2
+
+Python dictionary as data::
+
+ 1 2 {'weight':7, 'color':'green'}
+
+Arbitrary data::
+
+ 1 2 7 green
+
+For each edge (u, v) the node u is assigned to part 0 and the node v to part 1.
+"""
+
+__all__ = ["generate_edgelist", "write_edgelist", "parse_edgelist", "read_edgelist"]
+
+import networkx as nx
+from networkx.utils import not_implemented_for, open_file
+
+
+@open_file(1, mode="wb")
+def write_edgelist(G, path, comments="#", delimiter=" ", data=True, encoding="utf-8"):
+ """Write a bipartite graph as a list of edges.
+
+ Parameters
+ ----------
+ G : Graph
+ A NetworkX bipartite graph
+ path : file or string
+ File or filename to write. If a file is provided, it must be
+ opened in 'wb' mode. Filenames ending in .gz or .bz2 will be compressed.
+ comments : string, optional
+ The character used to indicate the start of a comment
+ delimiter : string, optional
+ The string used to separate values. The default is whitespace.
+ data : bool or list, optional
+ If False write no edge data.
+ If True write a string representation of the edge data dictionary..
+ If a list (or other iterable) is provided, write the keys specified
+ in the list.
+ encoding: string, optional
+ Specify which encoding to use when writing file.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> G.add_nodes_from([0, 2], bipartite=0)
+ >>> G.add_nodes_from([1, 3], bipartite=1)
+ >>> nx.write_edgelist(G, "test.edgelist")
+ >>> fh = open("test.edgelist", "wb")
+ >>> nx.write_edgelist(G, fh)
+ >>> nx.write_edgelist(G, "test.edgelist.gz")
+ >>> nx.write_edgelist(G, "test.edgelist.gz", data=False)
+
+ >>> G = nx.Graph()
+ >>> G.add_edge(1, 2, weight=7, color="red")
+ >>> nx.write_edgelist(G, "test.edgelist", data=False)
+ >>> nx.write_edgelist(G, "test.edgelist", data=["color"])
+ >>> nx.write_edgelist(G, "test.edgelist", data=["color", "weight"])
+
+ See Also
+ --------
+ write_edgelist
+ generate_edgelist
+ """
+ for line in generate_edgelist(G, delimiter, data):
+ line += "\n"
+ path.write(line.encode(encoding))
+
+
+@not_implemented_for("directed")
+def generate_edgelist(G, delimiter=" ", data=True):
+ """Generate a single line of the bipartite graph G in edge list format.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ The graph is assumed to have node attribute `part` set to 0,1 representing
+ the two graph parts
+
+ delimiter : string, optional
+ Separator for node labels
+
+ data : bool or list of keys
+ If False generate no edge data. If True use a dictionary
+ representation of edge data. If a list of keys use a list of data
+ values corresponding to the keys.
+
+ Returns
+ -------
+ lines : string
+ Lines of data in adjlist format.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.path_graph(4)
+ >>> G.add_nodes_from([0, 2], bipartite=0)
+ >>> G.add_nodes_from([1, 3], bipartite=1)
+ >>> G[1][2]["weight"] = 3
+ >>> G[2][3]["capacity"] = 12
+ >>> for line in bipartite.generate_edgelist(G, data=False):
+ ... print(line)
+ 0 1
+ 2 1
+ 2 3
+
+ >>> for line in bipartite.generate_edgelist(G):
+ ... print(line)
+ 0 1 {}
+ 2 1 {'weight': 3}
+ 2 3 {'capacity': 12}
+
+ >>> for line in bipartite.generate_edgelist(G, data=["weight"]):
+ ... print(line)
+ 0 1
+ 2 1 3
+ 2 3
+ """
+ try:
+ part0 = [n for n, d in G.nodes.items() if d["bipartite"] == 0]
+ except BaseException as err:
+ raise AttributeError("Missing node attribute `bipartite`") from err
+ if data is True or data is False:
+ for n in part0:
+ for edge in G.edges(n, data=data):
+ yield delimiter.join(map(str, edge))
+ else:
+ for n in part0:
+ for u, v, d in G.edges(n, data=True):
+ edge = [u, v]
+ try:
+ edge.extend(d[k] for k in data)
+ except KeyError:
+ pass # missing data for this edge, should warn?
+ yield delimiter.join(map(str, edge))
+
+
+@nx._dispatchable(name="bipartite_parse_edgelist", graphs=None, returns_graph=True)
+def parse_edgelist(
+ lines, comments="#", delimiter=None, create_using=None, nodetype=None, data=True
+):
+ """Parse lines of an edge list representation of a bipartite graph.
+
+ Parameters
+ ----------
+ lines : list or iterator of strings
+ Input data in edgelist format
+ comments : string, optional
+ Marker for comment lines
+ delimiter : string, optional
+ Separator for node labels
+ create_using: NetworkX graph container, optional
+ Use given NetworkX graph for holding nodes or edges.
+ nodetype : Python type, optional
+ Convert nodes to this type.
+ data : bool or list of (label,type) tuples
+ If False generate no edge data or if True use a dictionary
+ representation of edge data or a list tuples specifying dictionary
+ key names and types for edge data.
+
+ Returns
+ -------
+ G: NetworkX Graph
+ The bipartite graph corresponding to lines
+
+ Examples
+ --------
+ Edgelist with no data:
+
+ >>> from networkx.algorithms import bipartite
+ >>> lines = ["1 2", "2 3", "3 4"]
+ >>> G = bipartite.parse_edgelist(lines, nodetype=int)
+ >>> sorted(G.nodes())
+ [1, 2, 3, 4]
+ >>> sorted(G.nodes(data=True))
+ [(1, {'bipartite': 0}), (2, {'bipartite': 0}), (3, {'bipartite': 0}), (4, {'bipartite': 1})]
+ >>> sorted(G.edges())
+ [(1, 2), (2, 3), (3, 4)]
+
+ Edgelist with data in Python dictionary representation:
+
+ >>> lines = ["1 2 {'weight':3}", "2 3 {'weight':27}", "3 4 {'weight':3.0}"]
+ >>> G = bipartite.parse_edgelist(lines, nodetype=int)
+ >>> sorted(G.nodes())
+ [1, 2, 3, 4]
+ >>> sorted(G.edges(data=True))
+ [(1, 2, {'weight': 3}), (2, 3, {'weight': 27}), (3, 4, {'weight': 3.0})]
+
+ Edgelist with data in a list:
+
+ >>> lines = ["1 2 3", "2 3 27", "3 4 3.0"]
+ >>> G = bipartite.parse_edgelist(lines, nodetype=int, data=(("weight", float),))
+ >>> sorted(G.nodes())
+ [1, 2, 3, 4]
+ >>> sorted(G.edges(data=True))
+ [(1, 2, {'weight': 3.0}), (2, 3, {'weight': 27.0}), (3, 4, {'weight': 3.0})]
+
+ See Also
+ --------
+ """
+ from ast import literal_eval
+
+ G = nx.empty_graph(0, create_using)
+ for line in lines:
+ p = line.find(comments)
+ if p >= 0:
+ line = line[:p]
+ if not len(line):
+ continue
+ # split line, should have 2 or more
+ s = line.rstrip("\n").split(delimiter)
+ if len(s) < 2:
+ continue
+ u = s.pop(0)
+ v = s.pop(0)
+ d = s
+ if nodetype is not None:
+ try:
+ u = nodetype(u)
+ v = nodetype(v)
+ except BaseException as err:
+ raise TypeError(
+ f"Failed to convert nodes {u},{v} to type {nodetype}."
+ ) from err
+
+ if len(d) == 0 or data is False:
+ # no data or data type specified
+ edgedata = {}
+ elif data is True:
+ # no edge types specified
+ try: # try to evaluate as dictionary
+ edgedata = dict(literal_eval(" ".join(d)))
+ except BaseException as err:
+ raise TypeError(
+ f"Failed to convert edge data ({d}) to dictionary."
+ ) from err
+ else:
+ # convert edge data to dictionary with specified keys and type
+ if len(d) != len(data):
+ raise IndexError(
+ f"Edge data {d} and data_keys {data} are not the same length"
+ )
+ edgedata = {}
+ for (edge_key, edge_type), edge_value in zip(data, d):
+ try:
+ edge_value = edge_type(edge_value)
+ except BaseException as err:
+ raise TypeError(
+ f"Failed to convert {edge_key} data "
+ f"{edge_value} to type {edge_type}."
+ ) from err
+ edgedata.update({edge_key: edge_value})
+ G.add_node(u, bipartite=0)
+ G.add_node(v, bipartite=1)
+ G.add_edge(u, v, **edgedata)
+ return G
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(name="bipartite_read_edgelist", graphs=None, returns_graph=True)
+def read_edgelist(
+ path,
+ comments="#",
+ delimiter=None,
+ create_using=None,
+ nodetype=None,
+ data=True,
+ edgetype=None,
+ encoding="utf-8",
+):
+ """Read a bipartite graph from a list of edges.
+
+ Parameters
+ ----------
+ path : file or string
+ File or filename to read. If a file is provided, it must be
+ opened in 'rb' mode.
+ Filenames ending in .gz or .bz2 will be uncompressed.
+ comments : string, optional
+ The character used to indicate the start of a comment.
+ delimiter : string, optional
+ The string used to separate values. The default is whitespace.
+ create_using : Graph container, optional,
+ Use specified container to build graph. The default is networkx.Graph,
+ an undirected graph.
+ nodetype : int, float, str, Python type, optional
+ Convert node data from strings to specified type
+ data : bool or list of (label,type) tuples
+ Tuples specifying dictionary key names and types for edge data
+ edgetype : int, float, str, Python type, optional OBSOLETE
+ Convert edge data from strings to specified type and use as 'weight'
+ encoding: string, optional
+ Specify which encoding to use when reading file.
+
+ Returns
+ -------
+ G : graph
+ A networkx Graph or other type specified with create_using
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.path_graph(4)
+ >>> G.add_nodes_from([0, 2], bipartite=0)
+ >>> G.add_nodes_from([1, 3], bipartite=1)
+ >>> bipartite.write_edgelist(G, "test.edgelist")
+ >>> G = bipartite.read_edgelist("test.edgelist")
+
+ >>> fh = open("test.edgelist", "rb")
+ >>> G = bipartite.read_edgelist(fh)
+ >>> fh.close()
+
+ >>> G = bipartite.read_edgelist("test.edgelist", nodetype=int)
+
+ Edgelist with data in a list:
+
+ >>> textline = "1 2 3"
+ >>> fh = open("test.edgelist", "w")
+ >>> d = fh.write(textline)
+ >>> fh.close()
+ >>> G = bipartite.read_edgelist(
+ ... "test.edgelist", nodetype=int, data=(("weight", float),)
+ ... )
+ >>> list(G)
+ [1, 2]
+ >>> list(G.edges(data=True))
+ [(1, 2, {'weight': 3.0})]
+
+ See parse_edgelist() for more examples of formatting.
+
+ See Also
+ --------
+ parse_edgelist
+
+ Notes
+ -----
+ Since nodes must be hashable, the function nodetype must return hashable
+ types (e.g. int, float, str, frozenset - or tuples of those, etc.)
+ """
+ lines = (line.decode(encoding) for line in path)
+ return parse_edgelist(
+ lines,
+ comments=comments,
+ delimiter=delimiter,
+ create_using=create_using,
+ nodetype=nodetype,
+ data=data,
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/extendability.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/extendability.py
new file mode 100644
index 00000000..61d8d067
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/extendability.py
@@ -0,0 +1,105 @@
+"""Provides a function for computing the extendability of a graph which is
+undirected, simple, connected and bipartite and contains at least one perfect matching."""
+
+import networkx as nx
+from networkx.utils import not_implemented_for
+
+__all__ = ["maximal_extendability"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def maximal_extendability(G):
+ """Computes the extendability of a graph.
+
+ The extendability of a graph is defined as the maximum $k$ for which `G`
+ is $k$-extendable. Graph `G` is $k$-extendable if and only if `G` has a
+ perfect matching and every set of $k$ independent edges can be extended
+ to a perfect matching in `G`.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+ A fully-connected bipartite graph without self-loops
+
+ Returns
+ -------
+ extendability : int
+
+ Raises
+ ------
+ NetworkXError
+ If the graph `G` is disconnected.
+ If the graph `G` is not bipartite.
+ If the graph `G` does not contain a perfect matching.
+ If the residual graph of `G` is not strongly connected.
+
+ Notes
+ -----
+ Definition:
+ Let `G` be a simple, connected, undirected and bipartite graph with a perfect
+ matching M and bipartition (U,V). The residual graph of `G`, denoted by $G_M$,
+ is the graph obtained from G by directing the edges of M from V to U and the
+ edges that do not belong to M from U to V.
+
+ Lemma [1]_ :
+ Let M be a perfect matching of `G`. `G` is $k$-extendable if and only if its residual
+ graph $G_M$ is strongly connected and there are $k$ vertex-disjoint directed
+ paths between every vertex of U and every vertex of V.
+
+ Assuming that input graph `G` is undirected, simple, connected, bipartite and contains
+ a perfect matching M, this function constructs the residual graph $G_M$ of G and
+ returns the minimum value among the maximum vertex-disjoint directed paths between
+ every vertex of U and every vertex of V in $G_M$. By combining the definitions
+ and the lemma, this value represents the extendability of the graph `G`.
+
+ Time complexity O($n^3$ $m^2$)) where $n$ is the number of vertices
+ and $m$ is the number of edges.
+
+ References
+ ----------
+ .. [1] "A polynomial algorithm for the extendability problem in bipartite graphs",
+ J. Lakhal, L. Litzler, Information Processing Letters, 1998.
+ .. [2] "On n-extendible graphs", M. D. Plummer, Discrete Mathematics, 31:201–210, 1980
+ https://doi.org/10.1016/0012-365X(80)90037-0
+
+ """
+ if not nx.is_connected(G):
+ raise nx.NetworkXError("Graph G is not connected")
+
+ if not nx.bipartite.is_bipartite(G):
+ raise nx.NetworkXError("Graph G is not bipartite")
+
+ U, V = nx.bipartite.sets(G)
+
+ maximum_matching = nx.bipartite.hopcroft_karp_matching(G)
+
+ if not nx.is_perfect_matching(G, maximum_matching):
+ raise nx.NetworkXError("Graph G does not contain a perfect matching")
+
+ # list of edges in perfect matching, directed from V to U
+ pm = [(node, maximum_matching[node]) for node in V & maximum_matching.keys()]
+
+ # Direct all the edges of G, from V to U if in matching, else from U to V
+ directed_edges = [
+ (x, y) if (x in V and (x, y) in pm) or (x in U and (y, x) not in pm) else (y, x)
+ for x, y in G.edges
+ ]
+
+ # Construct the residual graph of G
+ residual_G = nx.DiGraph()
+ residual_G.add_nodes_from(G)
+ residual_G.add_edges_from(directed_edges)
+
+ if not nx.is_strongly_connected(residual_G):
+ raise nx.NetworkXError("The residual graph of G is not strongly connected")
+
+ # For node-pairs between V & U, keep min of max number of node-disjoint paths
+ # Variable $k$ stands for the extendability of graph G
+ k = float("inf")
+ for u in U:
+ for v in V:
+ num_paths = sum(1 for _ in nx.node_disjoint_paths(residual_G, u, v))
+ k = k if k < num_paths else num_paths
+ return k
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/generators.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/generators.py
new file mode 100644
index 00000000..e8428f6b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/generators.py
@@ -0,0 +1,604 @@
+"""
+Generators and functions for bipartite graphs.
+"""
+
+import math
+import numbers
+from functools import reduce
+
+import networkx as nx
+from networkx.utils import nodes_or_number, py_random_state
+
+__all__ = [
+ "configuration_model",
+ "havel_hakimi_graph",
+ "reverse_havel_hakimi_graph",
+ "alternating_havel_hakimi_graph",
+ "preferential_attachment_graph",
+ "random_graph",
+ "gnmk_random_graph",
+ "complete_bipartite_graph",
+]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number([0, 1])
+def complete_bipartite_graph(n1, n2, create_using=None):
+ """Returns the complete bipartite graph `K_{n_1,n_2}`.
+
+ The graph is composed of two partitions with nodes 0 to (n1 - 1)
+ in the first and nodes n1 to (n1 + n2 - 1) in the second.
+ Each node in the first is connected to each node in the second.
+
+ Parameters
+ ----------
+ n1, n2 : integer or iterable container of nodes
+ If integers, nodes are from `range(n1)` and `range(n1, n1 + n2)`.
+ If a container, the elements are the nodes.
+ create_using : NetworkX graph instance, (default: nx.Graph)
+ Return graph of this type.
+
+ Notes
+ -----
+ Nodes are the integers 0 to `n1 + n2 - 1` unless either n1 or n2 are
+ containers of nodes. If only one of n1 or n2 are integers, that
+ integer is replaced by `range` of that integer.
+
+ The nodes are assigned the attribute 'bipartite' with the value 0 or 1
+ to indicate which bipartite set the node belongs to.
+
+ This function is not imported in the main namespace.
+ To use it use nx.bipartite.complete_bipartite_graph
+ """
+ G = nx.empty_graph(0, create_using)
+ if G.is_directed():
+ raise nx.NetworkXError("Directed Graph not supported")
+
+ n1, top = n1
+ n2, bottom = n2
+ if isinstance(n1, numbers.Integral) and isinstance(n2, numbers.Integral):
+ bottom = [n1 + i for i in bottom]
+ G.add_nodes_from(top, bipartite=0)
+ G.add_nodes_from(bottom, bipartite=1)
+ if len(G) != len(top) + len(bottom):
+ raise nx.NetworkXError("Inputs n1 and n2 must contain distinct nodes")
+ G.add_edges_from((u, v) for u in top for v in bottom)
+ G.graph["name"] = f"complete_bipartite_graph({len(top)}, {len(bottom)})"
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(name="bipartite_configuration_model", graphs=None, returns_graph=True)
+def configuration_model(aseq, bseq, create_using=None, seed=None):
+ """Returns a random bipartite graph from two given degree sequences.
+
+ Parameters
+ ----------
+ aseq : list
+ Degree sequence for node set A.
+ bseq : list
+ Degree sequence for node set B.
+ create_using : NetworkX graph instance, optional
+ Return graph of this type.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ The graph is composed of two partitions. Set A has nodes 0 to
+ (len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
+ Nodes from set A are connected to nodes in set B by choosing
+ randomly from the possible free stubs, one in A and one in B.
+
+ Notes
+ -----
+ The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
+ If no graph type is specified use MultiGraph with parallel edges.
+ If you want a graph with no parallel edges use create_using=Graph()
+ but then the resulting degree sequences might not be exact.
+
+ The nodes are assigned the attribute 'bipartite' with the value 0 or 1
+ to indicate which bipartite set the node belongs to.
+
+ This function is not imported in the main namespace.
+ To use it use nx.bipartite.configuration_model
+ """
+ G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
+ if G.is_directed():
+ raise nx.NetworkXError("Directed Graph not supported")
+
+ # length and sum of each sequence
+ lena = len(aseq)
+ lenb = len(bseq)
+ suma = sum(aseq)
+ sumb = sum(bseq)
+
+ if not suma == sumb:
+ raise nx.NetworkXError(
+ f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
+ )
+
+ G = _add_nodes_with_bipartite_label(G, lena, lenb)
+
+ if len(aseq) == 0 or max(aseq) == 0:
+ return G # done if no edges
+
+ # build lists of degree-repeated vertex numbers
+ stubs = [[v] * aseq[v] for v in range(lena)]
+ astubs = [x for subseq in stubs for x in subseq]
+
+ stubs = [[v] * bseq[v - lena] for v in range(lena, lena + lenb)]
+ bstubs = [x for subseq in stubs for x in subseq]
+
+ # shuffle lists
+ seed.shuffle(astubs)
+ seed.shuffle(bstubs)
+
+ G.add_edges_from([astubs[i], bstubs[i]] for i in range(suma))
+
+ G.name = "bipartite_configuration_model"
+ return G
+
+
+@nx._dispatchable(name="bipartite_havel_hakimi_graph", graphs=None, returns_graph=True)
+def havel_hakimi_graph(aseq, bseq, create_using=None):
+ """Returns a bipartite graph from two given degree sequences using a
+ Havel-Hakimi style construction.
+
+ The graph is composed of two partitions. Set A has nodes 0 to
+ (len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
+ Nodes from the set A are connected to nodes in the set B by
+ connecting the highest degree nodes in set A to the highest degree
+ nodes in set B until all stubs are connected.
+
+ Parameters
+ ----------
+ aseq : list
+ Degree sequence for node set A.
+ bseq : list
+ Degree sequence for node set B.
+ create_using : NetworkX graph instance, optional
+ Return graph of this type.
+
+ Notes
+ -----
+ The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
+ If no graph type is specified use MultiGraph with parallel edges.
+ If you want a graph with no parallel edges use create_using=Graph()
+ but then the resulting degree sequences might not be exact.
+
+ The nodes are assigned the attribute 'bipartite' with the value 0 or 1
+ to indicate which bipartite set the node belongs to.
+
+ This function is not imported in the main namespace.
+ To use it use nx.bipartite.havel_hakimi_graph
+ """
+ G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
+ if G.is_directed():
+ raise nx.NetworkXError("Directed Graph not supported")
+
+ # length of the each sequence
+ naseq = len(aseq)
+ nbseq = len(bseq)
+
+ suma = sum(aseq)
+ sumb = sum(bseq)
+
+ if not suma == sumb:
+ raise nx.NetworkXError(
+ f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
+ )
+
+ G = _add_nodes_with_bipartite_label(G, naseq, nbseq)
+
+ if len(aseq) == 0 or max(aseq) == 0:
+ return G # done if no edges
+
+ # build list of degree-repeated vertex numbers
+ astubs = [[aseq[v], v] for v in range(naseq)]
+ bstubs = [[bseq[v - naseq], v] for v in range(naseq, naseq + nbseq)]
+ astubs.sort()
+ while astubs:
+ (degree, u) = astubs.pop() # take of largest degree node in the a set
+ if degree == 0:
+ break # done, all are zero
+ # connect the source to largest degree nodes in the b set
+ bstubs.sort()
+ for target in bstubs[-degree:]:
+ v = target[1]
+ G.add_edge(u, v)
+ target[0] -= 1 # note this updates bstubs too.
+ if target[0] == 0:
+ bstubs.remove(target)
+
+ G.name = "bipartite_havel_hakimi_graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def reverse_havel_hakimi_graph(aseq, bseq, create_using=None):
+ """Returns a bipartite graph from two given degree sequences using a
+ Havel-Hakimi style construction.
+
+ The graph is composed of two partitions. Set A has nodes 0 to
+ (len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
+ Nodes from set A are connected to nodes in the set B by connecting
+ the highest degree nodes in set A to the lowest degree nodes in
+ set B until all stubs are connected.
+
+ Parameters
+ ----------
+ aseq : list
+ Degree sequence for node set A.
+ bseq : list
+ Degree sequence for node set B.
+ create_using : NetworkX graph instance, optional
+ Return graph of this type.
+
+ Notes
+ -----
+ The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
+ If no graph type is specified use MultiGraph with parallel edges.
+ If you want a graph with no parallel edges use create_using=Graph()
+ but then the resulting degree sequences might not be exact.
+
+ The nodes are assigned the attribute 'bipartite' with the value 0 or 1
+ to indicate which bipartite set the node belongs to.
+
+ This function is not imported in the main namespace.
+ To use it use nx.bipartite.reverse_havel_hakimi_graph
+ """
+ G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
+ if G.is_directed():
+ raise nx.NetworkXError("Directed Graph not supported")
+
+ # length of the each sequence
+ lena = len(aseq)
+ lenb = len(bseq)
+ suma = sum(aseq)
+ sumb = sum(bseq)
+
+ if not suma == sumb:
+ raise nx.NetworkXError(
+ f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
+ )
+
+ G = _add_nodes_with_bipartite_label(G, lena, lenb)
+
+ if len(aseq) == 0 or max(aseq) == 0:
+ return G # done if no edges
+
+ # build list of degree-repeated vertex numbers
+ astubs = [[aseq[v], v] for v in range(lena)]
+ bstubs = [[bseq[v - lena], v] for v in range(lena, lena + lenb)]
+ astubs.sort()
+ bstubs.sort()
+ while astubs:
+ (degree, u) = astubs.pop() # take of largest degree node in the a set
+ if degree == 0:
+ break # done, all are zero
+ # connect the source to the smallest degree nodes in the b set
+ for target in bstubs[0:degree]:
+ v = target[1]
+ G.add_edge(u, v)
+ target[0] -= 1 # note this updates bstubs too.
+ if target[0] == 0:
+ bstubs.remove(target)
+
+ G.name = "bipartite_reverse_havel_hakimi_graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def alternating_havel_hakimi_graph(aseq, bseq, create_using=None):
+ """Returns a bipartite graph from two given degree sequences using
+ an alternating Havel-Hakimi style construction.
+
+ The graph is composed of two partitions. Set A has nodes 0 to
+ (len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
+ Nodes from the set A are connected to nodes in the set B by
+ connecting the highest degree nodes in set A to alternatively the
+ highest and the lowest degree nodes in set B until all stubs are
+ connected.
+
+ Parameters
+ ----------
+ aseq : list
+ Degree sequence for node set A.
+ bseq : list
+ Degree sequence for node set B.
+ create_using : NetworkX graph instance, optional
+ Return graph of this type.
+
+ Notes
+ -----
+ The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
+ If no graph type is specified use MultiGraph with parallel edges.
+ If you want a graph with no parallel edges use create_using=Graph()
+ but then the resulting degree sequences might not be exact.
+
+ The nodes are assigned the attribute 'bipartite' with the value 0 or 1
+ to indicate which bipartite set the node belongs to.
+
+ This function is not imported in the main namespace.
+ To use it use nx.bipartite.alternating_havel_hakimi_graph
+ """
+ G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
+ if G.is_directed():
+ raise nx.NetworkXError("Directed Graph not supported")
+
+ # length of the each sequence
+ naseq = len(aseq)
+ nbseq = len(bseq)
+ suma = sum(aseq)
+ sumb = sum(bseq)
+
+ if not suma == sumb:
+ raise nx.NetworkXError(
+ f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
+ )
+
+ G = _add_nodes_with_bipartite_label(G, naseq, nbseq)
+
+ if len(aseq) == 0 or max(aseq) == 0:
+ return G # done if no edges
+ # build list of degree-repeated vertex numbers
+ astubs = [[aseq[v], v] for v in range(naseq)]
+ bstubs = [[bseq[v - naseq], v] for v in range(naseq, naseq + nbseq)]
+ while astubs:
+ astubs.sort()
+ (degree, u) = astubs.pop() # take of largest degree node in the a set
+ if degree == 0:
+ break # done, all are zero
+ bstubs.sort()
+ small = bstubs[0 : degree // 2] # add these low degree targets
+ large = bstubs[(-degree + degree // 2) :] # now high degree targets
+ stubs = [x for z in zip(large, small) for x in z] # combine, sorry
+ if len(stubs) < len(small) + len(large): # check for zip truncation
+ stubs.append(large.pop())
+ for target in stubs:
+ v = target[1]
+ G.add_edge(u, v)
+ target[0] -= 1 # note this updates bstubs too.
+ if target[0] == 0:
+ bstubs.remove(target)
+
+ G.name = "bipartite_alternating_havel_hakimi_graph"
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def preferential_attachment_graph(aseq, p, create_using=None, seed=None):
+ """Create a bipartite graph with a preferential attachment model from
+ a given single degree sequence.
+
+ The graph is composed of two partitions. Set A has nodes 0 to
+ (len(aseq) - 1) and set B has nodes starting with node len(aseq).
+ The number of nodes in set B is random.
+
+ Parameters
+ ----------
+ aseq : list
+ Degree sequence for node set A.
+ p : float
+ Probability that a new bottom node is added.
+ create_using : NetworkX graph instance, optional
+ Return graph of this type.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ References
+ ----------
+ .. [1] Guillaume, J.L. and Latapy, M.,
+ Bipartite graphs as models of complex networks.
+ Physica A: Statistical Mechanics and its Applications,
+ 2006, 371(2), pp.795-813.
+ .. [2] Jean-Loup Guillaume and Matthieu Latapy,
+ Bipartite structure of all complex networks,
+ Inf. Process. Lett. 90, 2004, pg. 215-221
+ https://doi.org/10.1016/j.ipl.2004.03.007
+
+ Notes
+ -----
+ The nodes are assigned the attribute 'bipartite' with the value 0 or 1
+ to indicate which bipartite set the node belongs to.
+
+ This function is not imported in the main namespace.
+ To use it use nx.bipartite.preferential_attachment_graph
+ """
+ G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
+ if G.is_directed():
+ raise nx.NetworkXError("Directed Graph not supported")
+
+ if p > 1:
+ raise nx.NetworkXError(f"probability {p} > 1")
+
+ naseq = len(aseq)
+ G = _add_nodes_with_bipartite_label(G, naseq, 0)
+ vv = [[v] * aseq[v] for v in range(naseq)]
+ while vv:
+ while vv[0]:
+ source = vv[0][0]
+ vv[0].remove(source)
+ if seed.random() < p or len(G) == naseq:
+ target = len(G)
+ G.add_node(target, bipartite=1)
+ G.add_edge(source, target)
+ else:
+ bb = [[b] * G.degree(b) for b in range(naseq, len(G))]
+ # flatten the list of lists into a list.
+ bbstubs = reduce(lambda x, y: x + y, bb)
+ # choose preferentially a bottom node.
+ target = seed.choice(bbstubs)
+ G.add_node(target, bipartite=1)
+ G.add_edge(source, target)
+ vv.remove(vv[0])
+ G.name = "bipartite_preferential_attachment_model"
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_graph(n, m, p, seed=None, directed=False):
+ """Returns a bipartite random graph.
+
+ This is a bipartite version of the binomial (Erdős-Rényi) graph.
+ The graph is composed of two partitions. Set A has nodes 0 to
+ (n - 1) and set B has nodes n to (n + m - 1).
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes in the first bipartite set.
+ m : int
+ The number of nodes in the second bipartite set.
+ p : float
+ Probability for edge creation.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ directed : bool, optional (default=False)
+ If True return a directed graph
+
+ Notes
+ -----
+ The bipartite random graph algorithm chooses each of the n*m (undirected)
+ or 2*nm (directed) possible edges with probability p.
+
+ This algorithm is $O(n+m)$ where $m$ is the expected number of edges.
+
+ The nodes are assigned the attribute 'bipartite' with the value 0 or 1
+ to indicate which bipartite set the node belongs to.
+
+ This function is not imported in the main namespace.
+ To use it use nx.bipartite.random_graph
+
+ See Also
+ --------
+ gnp_random_graph, configuration_model
+
+ References
+ ----------
+ .. [1] Vladimir Batagelj and Ulrik Brandes,
+ "Efficient generation of large random networks",
+ Phys. Rev. E, 71, 036113, 2005.
+ """
+ G = nx.Graph()
+ G = _add_nodes_with_bipartite_label(G, n, m)
+ if directed:
+ G = nx.DiGraph(G)
+ G.name = f"fast_gnp_random_graph({n},{m},{p})"
+
+ if p <= 0:
+ return G
+ if p >= 1:
+ return nx.complete_bipartite_graph(n, m)
+
+ lp = math.log(1.0 - p)
+
+ v = 0
+ w = -1
+ while v < n:
+ lr = math.log(1.0 - seed.random())
+ w = w + 1 + int(lr / lp)
+ while w >= m and v < n:
+ w = w - m
+ v = v + 1
+ if v < n:
+ G.add_edge(v, n + w)
+
+ if directed:
+ # use the same algorithm to
+ # add edges from the "m" to "n" set
+ v = 0
+ w = -1
+ while v < n:
+ lr = math.log(1.0 - seed.random())
+ w = w + 1 + int(lr / lp)
+ while w >= m and v < n:
+ w = w - m
+ v = v + 1
+ if v < n:
+ G.add_edge(n + w, v)
+
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def gnmk_random_graph(n, m, k, seed=None, directed=False):
+ """Returns a random bipartite graph G_{n,m,k}.
+
+ Produces a bipartite graph chosen randomly out of the set of all graphs
+ with n top nodes, m bottom nodes, and k edges.
+ The graph is composed of two sets of nodes.
+ Set A has nodes 0 to (n - 1) and set B has nodes n to (n + m - 1).
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes in the first bipartite set.
+ m : int
+ The number of nodes in the second bipartite set.
+ k : int
+ The number of edges
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ directed : bool, optional (default=False)
+ If True return a directed graph
+
+ Examples
+ --------
+ from nx.algorithms import bipartite
+ G = bipartite.gnmk_random_graph(10,20,50)
+
+ See Also
+ --------
+ gnm_random_graph
+
+ Notes
+ -----
+ If k > m * n then a complete bipartite graph is returned.
+
+ This graph is a bipartite version of the `G_{nm}` random graph model.
+
+ The nodes are assigned the attribute 'bipartite' with the value 0 or 1
+ to indicate which bipartite set the node belongs to.
+
+ This function is not imported in the main namespace.
+ To use it use nx.bipartite.gnmk_random_graph
+ """
+ G = nx.Graph()
+ G = _add_nodes_with_bipartite_label(G, n, m)
+ if directed:
+ G = nx.DiGraph(G)
+ G.name = f"bipartite_gnm_random_graph({n},{m},{k})"
+ if n == 1 or m == 1:
+ return G
+ max_edges = n * m # max_edges for bipartite networks
+ if k >= max_edges: # Maybe we should raise an exception here
+ return nx.complete_bipartite_graph(n, m, create_using=G)
+
+ top = [n for n, d in G.nodes(data=True) if d["bipartite"] == 0]
+ bottom = list(set(G) - set(top))
+ edge_count = 0
+ while edge_count < k:
+ # generate random edge,u,v
+ u = seed.choice(top)
+ v = seed.choice(bottom)
+ if v in G[u]:
+ continue
+ else:
+ G.add_edge(u, v)
+ edge_count += 1
+ return G
+
+
+def _add_nodes_with_bipartite_label(G, lena, lenb):
+ G.add_nodes_from(range(lena + lenb))
+ b = dict(zip(range(lena), [0] * lena))
+ b.update(dict(zip(range(lena, lena + lenb), [1] * lenb)))
+ nx.set_node_attributes(G, b, "bipartite")
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/matching.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/matching.py
new file mode 100644
index 00000000..38a17478
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/matching.py
@@ -0,0 +1,590 @@
+# This module uses material from the Wikipedia article Hopcroft--Karp algorithm
+# <https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm>, accessed on
+# January 3, 2015, which is released under the Creative Commons
+# Attribution-Share-Alike License 3.0
+# <http://creativecommons.org/licenses/by-sa/3.0/>. That article includes
+# pseudocode, which has been translated into the corresponding Python code.
+#
+# Portions of this module use code from David Eppstein's Python Algorithms and
+# Data Structures (PADS) library, which is dedicated to the public domain (for
+# proof, see <http://www.ics.uci.edu/~eppstein/PADS/ABOUT-PADS.txt>).
+"""Provides functions for computing maximum cardinality matchings and minimum
+weight full matchings in a bipartite graph.
+
+If you don't care about the particular implementation of the maximum matching
+algorithm, simply use the :func:`maximum_matching`. If you do care, you can
+import one of the named maximum matching algorithms directly.
+
+For example, to find a maximum matching in the complete bipartite graph with
+two vertices on the left and three vertices on the right:
+
+>>> G = nx.complete_bipartite_graph(2, 3)
+>>> left, right = nx.bipartite.sets(G)
+>>> list(left)
+[0, 1]
+>>> list(right)
+[2, 3, 4]
+>>> nx.bipartite.maximum_matching(G)
+{0: 2, 1: 3, 2: 0, 3: 1}
+
+The dictionary returned by :func:`maximum_matching` includes a mapping for
+vertices in both the left and right vertex sets.
+
+Similarly, :func:`minimum_weight_full_matching` produces, for a complete
+weighted bipartite graph, a matching whose cardinality is the cardinality of
+the smaller of the two partitions, and for which the sum of the weights of the
+edges included in the matching is minimal.
+
+"""
+
+import collections
+import itertools
+
+import networkx as nx
+from networkx.algorithms.bipartite import sets as bipartite_sets
+from networkx.algorithms.bipartite.matrix import biadjacency_matrix
+
+__all__ = [
+ "maximum_matching",
+ "hopcroft_karp_matching",
+ "eppstein_matching",
+ "to_vertex_cover",
+ "minimum_weight_full_matching",
+]
+
+INFINITY = float("inf")
+
+
+@nx._dispatchable
+def hopcroft_karp_matching(G, top_nodes=None):
+ """Returns the maximum cardinality matching of the bipartite graph `G`.
+
+ A matching is a set of edges that do not share any nodes. A maximum
+ cardinality matching is a matching with the most edges possible. It
+ is not always unique. Finding a matching in a bipartite graph can be
+ treated as a networkx flow problem.
+
+ The functions ``hopcroft_karp_matching`` and ``maximum_matching``
+ are aliases of the same function.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ Undirected bipartite graph
+
+ top_nodes : container of nodes
+
+ Container with all nodes in one bipartite node set. If not supplied
+ it will be computed. But if more than one solution exists an exception
+ will be raised.
+
+ Returns
+ -------
+ matches : dictionary
+
+ The matching is returned as a dictionary, `matches`, such that
+ ``matches[v] == w`` if node `v` is matched to node `w`. Unmatched
+ nodes do not occur as a key in `matches`.
+
+ Raises
+ ------
+ AmbiguousSolution
+ Raised if the input bipartite graph is disconnected and no container
+ with all nodes in one bipartite set is provided. When determining
+ the nodes in each bipartite set more than one valid solution is
+ possible if the input graph is disconnected.
+
+ Notes
+ -----
+ This function is implemented with the `Hopcroft--Karp matching algorithm
+ <https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm>`_ for
+ bipartite graphs.
+
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ See Also
+ --------
+ maximum_matching
+ hopcroft_karp_matching
+ eppstein_matching
+
+ References
+ ----------
+ .. [1] John E. Hopcroft and Richard M. Karp. "An n^{5 / 2} Algorithm for
+ Maximum Matchings in Bipartite Graphs" In: **SIAM Journal of Computing**
+ 2.4 (1973), pp. 225--231. <https://doi.org/10.1137/0202019>.
+
+ """
+
+ # First we define some auxiliary search functions.
+ #
+ # If you are a human reading these auxiliary search functions, the "global"
+ # variables `leftmatches`, `rightmatches`, `distances`, etc. are defined
+ # below the functions, so that they are initialized close to the initial
+ # invocation of the search functions.
+ def breadth_first_search():
+ for v in left:
+ if leftmatches[v] is None:
+ distances[v] = 0
+ queue.append(v)
+ else:
+ distances[v] = INFINITY
+ distances[None] = INFINITY
+ while queue:
+ v = queue.popleft()
+ if distances[v] < distances[None]:
+ for u in G[v]:
+ if distances[rightmatches[u]] is INFINITY:
+ distances[rightmatches[u]] = distances[v] + 1
+ queue.append(rightmatches[u])
+ return distances[None] is not INFINITY
+
+ def depth_first_search(v):
+ if v is not None:
+ for u in G[v]:
+ if distances[rightmatches[u]] == distances[v] + 1:
+ if depth_first_search(rightmatches[u]):
+ rightmatches[u] = v
+ leftmatches[v] = u
+ return True
+ distances[v] = INFINITY
+ return False
+ return True
+
+ # Initialize the "global" variables that maintain state during the search.
+ left, right = bipartite_sets(G, top_nodes)
+ leftmatches = {v: None for v in left}
+ rightmatches = {v: None for v in right}
+ distances = {}
+ queue = collections.deque()
+
+ # Implementation note: this counter is incremented as pairs are matched but
+ # it is currently not used elsewhere in the computation.
+ num_matched_pairs = 0
+ while breadth_first_search():
+ for v in left:
+ if leftmatches[v] is None:
+ if depth_first_search(v):
+ num_matched_pairs += 1
+
+ # Strip the entries matched to `None`.
+ leftmatches = {k: v for k, v in leftmatches.items() if v is not None}
+ rightmatches = {k: v for k, v in rightmatches.items() if v is not None}
+
+ # At this point, the left matches and the right matches are inverses of one
+ # another. In other words,
+ #
+ # leftmatches == {v, k for k, v in rightmatches.items()}
+ #
+ # Finally, we combine both the left matches and right matches.
+ return dict(itertools.chain(leftmatches.items(), rightmatches.items()))
+
+
+@nx._dispatchable
+def eppstein_matching(G, top_nodes=None):
+ """Returns the maximum cardinality matching of the bipartite graph `G`.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ Undirected bipartite graph
+
+ top_nodes : container
+
+ Container with all nodes in one bipartite node set. If not supplied
+ it will be computed. But if more than one solution exists an exception
+ will be raised.
+
+ Returns
+ -------
+ matches : dictionary
+
+ The matching is returned as a dictionary, `matching`, such that
+ ``matching[v] == w`` if node `v` is matched to node `w`. Unmatched
+ nodes do not occur as a key in `matching`.
+
+ Raises
+ ------
+ AmbiguousSolution
+ Raised if the input bipartite graph is disconnected and no container
+ with all nodes in one bipartite set is provided. When determining
+ the nodes in each bipartite set more than one valid solution is
+ possible if the input graph is disconnected.
+
+ Notes
+ -----
+ This function is implemented with David Eppstein's version of the algorithm
+ Hopcroft--Karp algorithm (see :func:`hopcroft_karp_matching`), which
+ originally appeared in the `Python Algorithms and Data Structures library
+ (PADS) <http://www.ics.uci.edu/~eppstein/PADS/ABOUT-PADS.txt>`_.
+
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ See Also
+ --------
+
+ hopcroft_karp_matching
+
+ """
+ # Due to its original implementation, a directed graph is needed
+ # so that the two sets of bipartite nodes can be distinguished
+ left, right = bipartite_sets(G, top_nodes)
+ G = nx.DiGraph(G.edges(left))
+ # initialize greedy matching (redundant, but faster than full search)
+ matching = {}
+ for u in G:
+ for v in G[u]:
+ if v not in matching:
+ matching[v] = u
+ break
+ while True:
+ # structure residual graph into layers
+ # pred[u] gives the neighbor in the previous layer for u in U
+ # preds[v] gives a list of neighbors in the previous layer for v in V
+ # unmatched gives a list of unmatched vertices in final layer of V,
+ # and is also used as a flag value for pred[u] when u is in the first
+ # layer
+ preds = {}
+ unmatched = []
+ pred = {u: unmatched for u in G}
+ for v in matching:
+ del pred[matching[v]]
+ layer = list(pred)
+
+ # repeatedly extend layering structure by another pair of layers
+ while layer and not unmatched:
+ newLayer = {}
+ for u in layer:
+ for v in G[u]:
+ if v not in preds:
+ newLayer.setdefault(v, []).append(u)
+ layer = []
+ for v in newLayer:
+ preds[v] = newLayer[v]
+ if v in matching:
+ layer.append(matching[v])
+ pred[matching[v]] = v
+ else:
+ unmatched.append(v)
+
+ # did we finish layering without finding any alternating paths?
+ if not unmatched:
+ # TODO - The lines between --- were unused and were thus commented
+ # out. This whole commented chunk should be reviewed to determine
+ # whether it should be built upon or completely removed.
+ # ---
+ # unlayered = {}
+ # for u in G:
+ # # TODO Why is extra inner loop necessary?
+ # for v in G[u]:
+ # if v not in preds:
+ # unlayered[v] = None
+ # ---
+ # TODO Originally, this function returned a three-tuple:
+ #
+ # return (matching, list(pred), list(unlayered))
+ #
+ # For some reason, the documentation for this function
+ # indicated that the second and third elements of the returned
+ # three-tuple would be the vertices in the left and right vertex
+ # sets, respectively, that are also in the maximum independent set.
+ # However, what I think the author meant was that the second
+ # element is the list of vertices that were unmatched and the third
+ # element was the list of vertices that were matched. Since that
+ # seems to be the case, they don't really need to be returned,
+ # since that information can be inferred from the matching
+ # dictionary.
+
+ # All the matched nodes must be a key in the dictionary
+ for key in matching.copy():
+ matching[matching[key]] = key
+ return matching
+
+ # recursively search backward through layers to find alternating paths
+ # recursion returns true if found path, false otherwise
+ def recurse(v):
+ if v in preds:
+ L = preds.pop(v)
+ for u in L:
+ if u in pred:
+ pu = pred.pop(u)
+ if pu is unmatched or recurse(pu):
+ matching[v] = u
+ return True
+ return False
+
+ for v in unmatched:
+ recurse(v)
+
+
+def _is_connected_by_alternating_path(G, v, matched_edges, unmatched_edges, targets):
+ """Returns True if and only if the vertex `v` is connected to one of
+ the target vertices by an alternating path in `G`.
+
+ An *alternating path* is a path in which every other edge is in the
+ specified maximum matching (and the remaining edges in the path are not in
+ the matching). An alternating path may have matched edges in the even
+ positions or in the odd positions, as long as the edges alternate between
+ 'matched' and 'unmatched'.
+
+ `G` is an undirected bipartite NetworkX graph.
+
+ `v` is a vertex in `G`.
+
+ `matched_edges` is a set of edges present in a maximum matching in `G`.
+
+ `unmatched_edges` is a set of edges not present in a maximum
+ matching in `G`.
+
+ `targets` is a set of vertices.
+
+ """
+
+ def _alternating_dfs(u, along_matched=True):
+ """Returns True if and only if `u` is connected to one of the
+ targets by an alternating path.
+
+ `u` is a vertex in the graph `G`.
+
+ If `along_matched` is True, this step of the depth-first search
+ will continue only through edges in the given matching. Otherwise, it
+ will continue only through edges *not* in the given matching.
+
+ """
+ visited = set()
+ # Follow matched edges when depth is even,
+ # and follow unmatched edges when depth is odd.
+ initial_depth = 0 if along_matched else 1
+ stack = [(u, iter(G[u]), initial_depth)]
+ while stack:
+ parent, children, depth = stack[-1]
+ valid_edges = matched_edges if depth % 2 else unmatched_edges
+ try:
+ child = next(children)
+ if child not in visited:
+ if (parent, child) in valid_edges or (child, parent) in valid_edges:
+ if child in targets:
+ return True
+ visited.add(child)
+ stack.append((child, iter(G[child]), depth + 1))
+ except StopIteration:
+ stack.pop()
+ return False
+
+ # Check for alternating paths starting with edges in the matching, then
+ # check for alternating paths starting with edges not in the
+ # matching.
+ return _alternating_dfs(v, along_matched=True) or _alternating_dfs(
+ v, along_matched=False
+ )
+
+
+def _connected_by_alternating_paths(G, matching, targets):
+ """Returns the set of vertices that are connected to one of the target
+ vertices by an alternating path in `G` or are themselves a target.
+
+ An *alternating path* is a path in which every other edge is in the
+ specified maximum matching (and the remaining edges in the path are not in
+ the matching). An alternating path may have matched edges in the even
+ positions or in the odd positions, as long as the edges alternate between
+ 'matched' and 'unmatched'.
+
+ `G` is an undirected bipartite NetworkX graph.
+
+ `matching` is a dictionary representing a maximum matching in `G`, as
+ returned by, for example, :func:`maximum_matching`.
+
+ `targets` is a set of vertices.
+
+ """
+ # Get the set of matched edges and the set of unmatched edges. Only include
+ # one version of each undirected edge (for example, include edge (1, 2) but
+ # not edge (2, 1)). Using frozensets as an intermediary step we do not
+ # require nodes to be orderable.
+ edge_sets = {frozenset((u, v)) for u, v in matching.items()}
+ matched_edges = {tuple(edge) for edge in edge_sets}
+ unmatched_edges = {
+ (u, v) for (u, v) in G.edges() if frozenset((u, v)) not in edge_sets
+ }
+
+ return {
+ v
+ for v in G
+ if v in targets
+ or _is_connected_by_alternating_path(
+ G, v, matched_edges, unmatched_edges, targets
+ )
+ }
+
+
+@nx._dispatchable
+def to_vertex_cover(G, matching, top_nodes=None):
+ """Returns the minimum vertex cover corresponding to the given maximum
+ matching of the bipartite graph `G`.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ Undirected bipartite graph
+
+ matching : dictionary
+
+ A dictionary whose keys are vertices in `G` and whose values are the
+ distinct neighbors comprising the maximum matching for `G`, as returned
+ by, for example, :func:`maximum_matching`. The dictionary *must*
+ represent the maximum matching.
+
+ top_nodes : container
+
+ Container with all nodes in one bipartite node set. If not supplied
+ it will be computed. But if more than one solution exists an exception
+ will be raised.
+
+ Returns
+ -------
+ vertex_cover : :class:`set`
+
+ The minimum vertex cover in `G`.
+
+ Raises
+ ------
+ AmbiguousSolution
+ Raised if the input bipartite graph is disconnected and no container
+ with all nodes in one bipartite set is provided. When determining
+ the nodes in each bipartite set more than one valid solution is
+ possible if the input graph is disconnected.
+
+ Notes
+ -----
+ This function is implemented using the procedure guaranteed by `Konig's
+ theorem
+ <https://en.wikipedia.org/wiki/K%C3%B6nig%27s_theorem_%28graph_theory%29>`_,
+ which proves an equivalence between a maximum matching and a minimum vertex
+ cover in bipartite graphs.
+
+ Since a minimum vertex cover is the complement of a maximum independent set
+ for any graph, one can compute the maximum independent set of a bipartite
+ graph this way:
+
+ >>> G = nx.complete_bipartite_graph(2, 3)
+ >>> matching = nx.bipartite.maximum_matching(G)
+ >>> vertex_cover = nx.bipartite.to_vertex_cover(G, matching)
+ >>> independent_set = set(G) - vertex_cover
+ >>> print(list(independent_set))
+ [2, 3, 4]
+
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ """
+ # This is a Python implementation of the algorithm described at
+ # <https://en.wikipedia.org/wiki/K%C3%B6nig%27s_theorem_%28graph_theory%29#Proof>.
+ L, R = bipartite_sets(G, top_nodes)
+ # Let U be the set of unmatched vertices in the left vertex set.
+ unmatched_vertices = set(G) - set(matching)
+ U = unmatched_vertices & L
+ # Let Z be the set of vertices that are either in U or are connected to U
+ # by alternating paths.
+ Z = _connected_by_alternating_paths(G, matching, U)
+ # At this point, every edge either has a right endpoint in Z or a left
+ # endpoint not in Z. This gives us the vertex cover.
+ return (L - Z) | (R & Z)
+
+
+#: Returns the maximum cardinality matching in the given bipartite graph.
+#:
+#: This function is simply an alias for :func:`hopcroft_karp_matching`.
+maximum_matching = hopcroft_karp_matching
+
+
+@nx._dispatchable(edge_attrs="weight")
+def minimum_weight_full_matching(G, top_nodes=None, weight="weight"):
+ r"""Returns a minimum weight full matching of the bipartite graph `G`.
+
+ Let :math:`G = ((U, V), E)` be a weighted bipartite graph with real weights
+ :math:`w : E \to \mathbb{R}`. This function then produces a matching
+ :math:`M \subseteq E` with cardinality
+
+ .. math::
+ \lvert M \rvert = \min(\lvert U \rvert, \lvert V \rvert),
+
+ which minimizes the sum of the weights of the edges included in the
+ matching, :math:`\sum_{e \in M} w(e)`, or raises an error if no such
+ matching exists.
+
+ When :math:`\lvert U \rvert = \lvert V \rvert`, this is commonly
+ referred to as a perfect matching; here, since we allow
+ :math:`\lvert U \rvert` and :math:`\lvert V \rvert` to differ, we
+ follow Karp [1]_ and refer to the matching as *full*.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ Undirected bipartite graph
+
+ top_nodes : container
+
+ Container with all nodes in one bipartite node set. If not supplied
+ it will be computed.
+
+ weight : string, optional (default='weight')
+
+ The edge data key used to provide each value in the matrix.
+ If None, then each edge has weight 1.
+
+ Returns
+ -------
+ matches : dictionary
+
+ The matching is returned as a dictionary, `matches`, such that
+ ``matches[v] == w`` if node `v` is matched to node `w`. Unmatched
+ nodes do not occur as a key in `matches`.
+
+ Raises
+ ------
+ ValueError
+ Raised if no full matching exists.
+
+ ImportError
+ Raised if SciPy is not available.
+
+ Notes
+ -----
+ The problem of determining a minimum weight full matching is also known as
+ the rectangular linear assignment problem. This implementation defers the
+ calculation of the assignment to SciPy.
+
+ References
+ ----------
+ .. [1] Richard Manning Karp:
+ An algorithm to Solve the m x n Assignment Problem in Expected Time
+ O(mn log n).
+ Networks, 10(2):143–152, 1980.
+
+ """
+ import numpy as np
+ import scipy as sp
+
+ left, right = nx.bipartite.sets(G, top_nodes)
+ U = list(left)
+ V = list(right)
+ # We explicitly create the biadjacency matrix having infinities
+ # where edges are missing (as opposed to zeros, which is what one would
+ # get by using toarray on the sparse matrix).
+ weights_sparse = biadjacency_matrix(
+ G, row_order=U, column_order=V, weight=weight, format="coo"
+ )
+ weights = np.full(weights_sparse.shape, np.inf)
+ weights[weights_sparse.row, weights_sparse.col] = weights_sparse.data
+ left_matches = sp.optimize.linear_sum_assignment(weights)
+ d = {U[u]: V[v] for u, v in zip(*left_matches)}
+ # d will contain the matching from edges in left to right; we need to
+ # add the ones from right to left as well.
+ d.update({v: u for u, v in d.items()})
+ return d
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/matrix.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/matrix.py
new file mode 100644
index 00000000..bbfa47c7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/matrix.py
@@ -0,0 +1,168 @@
+"""
+====================
+Biadjacency matrices
+====================
+"""
+
+import itertools
+
+import networkx as nx
+from networkx.convert_matrix import _generate_weighted_edges
+
+__all__ = ["biadjacency_matrix", "from_biadjacency_matrix"]
+
+
+@nx._dispatchable(edge_attrs="weight")
+def biadjacency_matrix(
+ G, row_order, column_order=None, dtype=None, weight="weight", format="csr"
+):
+ r"""Returns the biadjacency matrix of the bipartite graph G.
+
+ Let `G = (U, V, E)` be a bipartite graph with node sets
+ `U = u_{1},...,u_{r}` and `V = v_{1},...,v_{s}`. The biadjacency
+ matrix [1]_ is the `r` x `s` matrix `B` in which `b_{i,j} = 1`
+ if, and only if, `(u_i, v_j) \in E`. If the parameter `weight` is
+ not `None` and matches the name of an edge attribute, its value is
+ used instead of 1.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ row_order : list of nodes
+ The rows of the matrix are ordered according to the list of nodes.
+
+ column_order : list, optional
+ The columns of the matrix are ordered according to the list of nodes.
+ If column_order is None, then the ordering of columns is arbitrary.
+
+ dtype : NumPy data-type, optional
+ A valid NumPy dtype used to initialize the array. If None, then the
+ NumPy default is used.
+
+ weight : string or None, optional (default='weight')
+ The edge data key used to provide each value in the matrix.
+ If None, then each edge has weight 1.
+
+ format : str in {'bsr', 'csr', 'csc', 'coo', 'lil', 'dia', 'dok'}
+ The type of the matrix to be returned (default 'csr'). For
+ some algorithms different implementations of sparse matrices
+ can perform better. See [2]_ for details.
+
+ Returns
+ -------
+ M : SciPy sparse array
+ Biadjacency matrix representation of the bipartite graph G.
+
+ Notes
+ -----
+ No attempt is made to check that the input graph is bipartite.
+
+ For directed bipartite graphs only successors are considered as neighbors.
+ To obtain an adjacency matrix with ones (or weight values) for both
+ predecessors and successors you have to generate two biadjacency matrices
+ where the rows of one of them are the columns of the other, and then add
+ one to the transpose of the other.
+
+ See Also
+ --------
+ adjacency_matrix
+ from_biadjacency_matrix
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Adjacency_matrix#Adjacency_matrix_of_a_bipartite_graph
+ .. [2] Scipy Dev. References, "Sparse Matrices",
+ https://docs.scipy.org/doc/scipy/reference/sparse.html
+ """
+ import scipy as sp
+
+ nlen = len(row_order)
+ if nlen == 0:
+ raise nx.NetworkXError("row_order is empty list")
+ if len(row_order) != len(set(row_order)):
+ msg = "Ambiguous ordering: `row_order` contained duplicates."
+ raise nx.NetworkXError(msg)
+ if column_order is None:
+ column_order = list(set(G) - set(row_order))
+ mlen = len(column_order)
+ if len(column_order) != len(set(column_order)):
+ msg = "Ambiguous ordering: `column_order` contained duplicates."
+ raise nx.NetworkXError(msg)
+
+ row_index = dict(zip(row_order, itertools.count()))
+ col_index = dict(zip(column_order, itertools.count()))
+
+ if G.number_of_edges() == 0:
+ row, col, data = [], [], []
+ else:
+ row, col, data = zip(
+ *(
+ (row_index[u], col_index[v], d.get(weight, 1))
+ for u, v, d in G.edges(row_order, data=True)
+ if u in row_index and v in col_index
+ )
+ )
+ A = sp.sparse.coo_array((data, (row, col)), shape=(nlen, mlen), dtype=dtype)
+ try:
+ return A.asformat(format)
+ except ValueError as err:
+ raise nx.NetworkXError(f"Unknown sparse array format: {format}") from err
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def from_biadjacency_matrix(A, create_using=None, edge_attribute="weight"):
+ r"""Creates a new bipartite graph from a biadjacency matrix given as a
+ SciPy sparse array.
+
+ Parameters
+ ----------
+ A: scipy sparse array
+ A biadjacency matrix representation of a graph
+
+ create_using: NetworkX graph
+ Use specified graph for result. The default is Graph()
+
+ edge_attribute: string
+ Name of edge attribute to store matrix numeric value. The data will
+ have the same type as the matrix entry (int, float, (real,imag)).
+
+ Notes
+ -----
+ The nodes are labeled with the attribute `bipartite` set to an integer
+ 0 or 1 representing membership in part 0 or part 1 of the bipartite graph.
+
+ If `create_using` is an instance of :class:`networkx.MultiGraph` or
+ :class:`networkx.MultiDiGraph` and the entries of `A` are of
+ type :class:`int`, then this function returns a multigraph (of the same
+ type as `create_using`) with parallel edges. In this case, `edge_attribute`
+ will be ignored.
+
+ See Also
+ --------
+ biadjacency_matrix
+ from_numpy_array
+
+ References
+ ----------
+ [1] https://en.wikipedia.org/wiki/Adjacency_matrix#Adjacency_matrix_of_a_bipartite_graph
+ """
+ G = nx.empty_graph(0, create_using)
+ n, m = A.shape
+ # Make sure we get even the isolated nodes of the graph.
+ G.add_nodes_from(range(n), bipartite=0)
+ G.add_nodes_from(range(n, n + m), bipartite=1)
+ # Create an iterable over (u, v, w) triples and for each triple, add an
+ # edge from u to v with weight w.
+ triples = ((u, n + v, d) for (u, v, d) in _generate_weighted_edges(A))
+ # If the entries in the adjacency matrix are integers and the graph is a
+ # multigraph, then create parallel edges, each with weight 1, for each
+ # entry in the adjacency matrix. Otherwise, create one edge for each
+ # positive entry in the adjacency matrix and set the weight of that edge to
+ # be the entry in the matrix.
+ if A.dtype.kind in ("i", "u") and G.is_multigraph():
+ chain = itertools.chain.from_iterable
+ triples = chain(((u, v, 1) for d in range(w)) for (u, v, w) in triples)
+ G.add_weighted_edges_from(triples, weight=edge_attribute)
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/projection.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/projection.py
new file mode 100644
index 00000000..7c2a26cf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/projection.py
@@ -0,0 +1,526 @@
+"""One-mode (unipartite) projections of bipartite graphs."""
+
+import networkx as nx
+from networkx.exception import NetworkXAlgorithmError
+from networkx.utils import not_implemented_for
+
+__all__ = [
+ "projected_graph",
+ "weighted_projected_graph",
+ "collaboration_weighted_projected_graph",
+ "overlap_weighted_projected_graph",
+ "generic_weighted_projected_graph",
+]
+
+
+@nx._dispatchable(
+ graphs="B", preserve_node_attrs=True, preserve_graph_attrs=True, returns_graph=True
+)
+def projected_graph(B, nodes, multigraph=False):
+ r"""Returns the projection of B onto one of its node sets.
+
+ Returns the graph G that is the projection of the bipartite graph B
+ onto the specified nodes. They retain their attributes and are connected
+ in G if they have a common neighbor in B.
+
+ Parameters
+ ----------
+ B : NetworkX graph
+ The input graph should be bipartite.
+
+ nodes : list or iterable
+ Nodes to project onto (the "bottom" nodes).
+
+ multigraph: bool (default=False)
+ If True return a multigraph where the multiple edges represent multiple
+ shared neighbors. They edge key in the multigraph is assigned to the
+ label of the neighbor.
+
+ Returns
+ -------
+ Graph : NetworkX graph or multigraph
+ A graph that is the projection onto the given nodes.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> B = nx.path_graph(4)
+ >>> G = bipartite.projected_graph(B, [1, 3])
+ >>> list(G)
+ [1, 3]
+ >>> list(G.edges())
+ [(1, 3)]
+
+ If nodes `a`, and `b` are connected through both nodes 1 and 2 then
+ building a multigraph results in two edges in the projection onto
+ [`a`, `b`]:
+
+ >>> B = nx.Graph()
+ >>> B.add_edges_from([("a", 1), ("b", 1), ("a", 2), ("b", 2)])
+ >>> G = bipartite.projected_graph(B, ["a", "b"], multigraph=True)
+ >>> print([sorted((u, v)) for u, v in G.edges()])
+ [['a', 'b'], ['a', 'b']]
+
+ Notes
+ -----
+ No attempt is made to verify that the input graph B is bipartite.
+ Returns a simple graph that is the projection of the bipartite graph B
+ onto the set of nodes given in list nodes. If multigraph=True then
+ a multigraph is returned with an edge for every shared neighbor.
+
+ Directed graphs are allowed as input. The output will also then
+ be a directed graph with edges if there is a directed path between
+ the nodes.
+
+ The graph and node properties are (shallow) copied to the projected graph.
+
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ See Also
+ --------
+ is_bipartite,
+ is_bipartite_node_set,
+ sets,
+ weighted_projected_graph,
+ collaboration_weighted_projected_graph,
+ overlap_weighted_projected_graph,
+ generic_weighted_projected_graph
+ """
+ if B.is_multigraph():
+ raise nx.NetworkXError("not defined for multigraphs")
+ if B.is_directed():
+ directed = True
+ if multigraph:
+ G = nx.MultiDiGraph()
+ else:
+ G = nx.DiGraph()
+ else:
+ directed = False
+ if multigraph:
+ G = nx.MultiGraph()
+ else:
+ G = nx.Graph()
+ G.graph.update(B.graph)
+ G.add_nodes_from((n, B.nodes[n]) for n in nodes)
+ for u in nodes:
+ nbrs2 = {v for nbr in B[u] for v in B[nbr] if v != u}
+ if multigraph:
+ for n in nbrs2:
+ if directed:
+ links = set(B[u]) & set(B.pred[n])
+ else:
+ links = set(B[u]) & set(B[n])
+ for l in links:
+ if not G.has_edge(u, n, l):
+ G.add_edge(u, n, key=l)
+ else:
+ G.add_edges_from((u, n) for n in nbrs2)
+ return G
+
+
+@not_implemented_for("multigraph")
+@nx._dispatchable(graphs="B", returns_graph=True)
+def weighted_projected_graph(B, nodes, ratio=False):
+ r"""Returns a weighted projection of B onto one of its node sets.
+
+ The weighted projected graph is the projection of the bipartite
+ network B onto the specified nodes with weights representing the
+ number of shared neighbors or the ratio between actual shared
+ neighbors and possible shared neighbors if ``ratio is True`` [1]_.
+ The nodes retain their attributes and are connected in the resulting
+ graph if they have an edge to a common node in the original graph.
+
+ Parameters
+ ----------
+ B : NetworkX graph
+ The input graph should be bipartite.
+
+ nodes : list or iterable
+ Distinct nodes to project onto (the "bottom" nodes).
+
+ ratio: Bool (default=False)
+ If True, edge weight is the ratio between actual shared neighbors
+ and maximum possible shared neighbors (i.e., the size of the other
+ node set). If False, edges weight is the number of shared neighbors.
+
+ Returns
+ -------
+ Graph : NetworkX graph
+ A graph that is the projection onto the given nodes.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> B = nx.path_graph(4)
+ >>> G = bipartite.weighted_projected_graph(B, [1, 3])
+ >>> list(G)
+ [1, 3]
+ >>> list(G.edges(data=True))
+ [(1, 3, {'weight': 1})]
+ >>> G = bipartite.weighted_projected_graph(B, [1, 3], ratio=True)
+ >>> list(G.edges(data=True))
+ [(1, 3, {'weight': 0.5})]
+
+ Notes
+ -----
+ No attempt is made to verify that the input graph B is bipartite, or that
+ the input nodes are distinct. However, if the length of the input nodes is
+ greater than or equal to the nodes in the graph B, an exception is raised.
+ If the nodes are not distinct but don't raise this error, the output weights
+ will be incorrect.
+ The graph and node properties are (shallow) copied to the projected graph.
+
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ See Also
+ --------
+ is_bipartite,
+ is_bipartite_node_set,
+ sets,
+ collaboration_weighted_projected_graph,
+ overlap_weighted_projected_graph,
+ generic_weighted_projected_graph
+ projected_graph
+
+ References
+ ----------
+ .. [1] Borgatti, S.P. and Halgin, D. In press. "Analyzing Affiliation
+ Networks". In Carrington, P. and Scott, J. (eds) The Sage Handbook
+ of Social Network Analysis. Sage Publications.
+ """
+ if B.is_directed():
+ pred = B.pred
+ G = nx.DiGraph()
+ else:
+ pred = B.adj
+ G = nx.Graph()
+ G.graph.update(B.graph)
+ G.add_nodes_from((n, B.nodes[n]) for n in nodes)
+ n_top = len(B) - len(nodes)
+
+ if n_top < 1:
+ raise NetworkXAlgorithmError(
+ f"the size of the nodes to project onto ({len(nodes)}) is >= the graph size ({len(B)}).\n"
+ "They are either not a valid bipartite partition or contain duplicates"
+ )
+
+ for u in nodes:
+ unbrs = set(B[u])
+ nbrs2 = {n for nbr in unbrs for n in B[nbr]} - {u}
+ for v in nbrs2:
+ vnbrs = set(pred[v])
+ common = unbrs & vnbrs
+ if not ratio:
+ weight = len(common)
+ else:
+ weight = len(common) / n_top
+ G.add_edge(u, v, weight=weight)
+ return G
+
+
+@not_implemented_for("multigraph")
+@nx._dispatchable(graphs="B", returns_graph=True)
+def collaboration_weighted_projected_graph(B, nodes):
+ r"""Newman's weighted projection of B onto one of its node sets.
+
+ The collaboration weighted projection is the projection of the
+ bipartite network B onto the specified nodes with weights assigned
+ using Newman's collaboration model [1]_:
+
+ .. math::
+
+ w_{u, v} = \sum_k \frac{\delta_{u}^{k} \delta_{v}^{k}}{d_k - 1}
+
+ where `u` and `v` are nodes from the bottom bipartite node set,
+ and `k` is a node of the top node set.
+ The value `d_k` is the degree of node `k` in the bipartite
+ network and `\delta_{u}^{k}` is 1 if node `u` is
+ linked to node `k` in the original bipartite graph or 0 otherwise.
+
+ The nodes retain their attributes and are connected in the resulting
+ graph if have an edge to a common node in the original bipartite
+ graph.
+
+ Parameters
+ ----------
+ B : NetworkX graph
+ The input graph should be bipartite.
+
+ nodes : list or iterable
+ Nodes to project onto (the "bottom" nodes).
+
+ Returns
+ -------
+ Graph : NetworkX graph
+ A graph that is the projection onto the given nodes.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> B = nx.path_graph(5)
+ >>> B.add_edge(1, 5)
+ >>> G = bipartite.collaboration_weighted_projected_graph(B, [0, 2, 4, 5])
+ >>> list(G)
+ [0, 2, 4, 5]
+ >>> for edge in sorted(G.edges(data=True)):
+ ... print(edge)
+ (0, 2, {'weight': 0.5})
+ (0, 5, {'weight': 0.5})
+ (2, 4, {'weight': 1.0})
+ (2, 5, {'weight': 0.5})
+
+ Notes
+ -----
+ No attempt is made to verify that the input graph B is bipartite.
+ The graph and node properties are (shallow) copied to the projected graph.
+
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ See Also
+ --------
+ is_bipartite,
+ is_bipartite_node_set,
+ sets,
+ weighted_projected_graph,
+ overlap_weighted_projected_graph,
+ generic_weighted_projected_graph,
+ projected_graph
+
+ References
+ ----------
+ .. [1] Scientific collaboration networks: II.
+ Shortest paths, weighted networks, and centrality,
+ M. E. J. Newman, Phys. Rev. E 64, 016132 (2001).
+ """
+ if B.is_directed():
+ pred = B.pred
+ G = nx.DiGraph()
+ else:
+ pred = B.adj
+ G = nx.Graph()
+ G.graph.update(B.graph)
+ G.add_nodes_from((n, B.nodes[n]) for n in nodes)
+ for u in nodes:
+ unbrs = set(B[u])
+ nbrs2 = {n for nbr in unbrs for n in B[nbr] if n != u}
+ for v in nbrs2:
+ vnbrs = set(pred[v])
+ common_degree = (len(B[n]) for n in unbrs & vnbrs)
+ weight = sum(1.0 / (deg - 1) for deg in common_degree if deg > 1)
+ G.add_edge(u, v, weight=weight)
+ return G
+
+
+@not_implemented_for("multigraph")
+@nx._dispatchable(graphs="B", returns_graph=True)
+def overlap_weighted_projected_graph(B, nodes, jaccard=True):
+ r"""Overlap weighted projection of B onto one of its node sets.
+
+ The overlap weighted projection is the projection of the bipartite
+ network B onto the specified nodes with weights representing
+ the Jaccard index between the neighborhoods of the two nodes in the
+ original bipartite network [1]_:
+
+ .. math::
+
+ w_{v, u} = \frac{|N(u) \cap N(v)|}{|N(u) \cup N(v)|}
+
+ or if the parameter 'jaccard' is False, the fraction of common
+ neighbors by minimum of both nodes degree in the original
+ bipartite graph [1]_:
+
+ .. math::
+
+ w_{v, u} = \frac{|N(u) \cap N(v)|}{min(|N(u)|, |N(v)|)}
+
+ The nodes retain their attributes and are connected in the resulting
+ graph if have an edge to a common node in the original bipartite graph.
+
+ Parameters
+ ----------
+ B : NetworkX graph
+ The input graph should be bipartite.
+
+ nodes : list or iterable
+ Nodes to project onto (the "bottom" nodes).
+
+ jaccard: Bool (default=True)
+
+ Returns
+ -------
+ Graph : NetworkX graph
+ A graph that is the projection onto the given nodes.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> B = nx.path_graph(5)
+ >>> nodes = [0, 2, 4]
+ >>> G = bipartite.overlap_weighted_projected_graph(B, nodes)
+ >>> list(G)
+ [0, 2, 4]
+ >>> list(G.edges(data=True))
+ [(0, 2, {'weight': 0.5}), (2, 4, {'weight': 0.5})]
+ >>> G = bipartite.overlap_weighted_projected_graph(B, nodes, jaccard=False)
+ >>> list(G.edges(data=True))
+ [(0, 2, {'weight': 1.0}), (2, 4, {'weight': 1.0})]
+
+ Notes
+ -----
+ No attempt is made to verify that the input graph B is bipartite.
+ The graph and node properties are (shallow) copied to the projected graph.
+
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ See Also
+ --------
+ is_bipartite,
+ is_bipartite_node_set,
+ sets,
+ weighted_projected_graph,
+ collaboration_weighted_projected_graph,
+ generic_weighted_projected_graph,
+ projected_graph
+
+ References
+ ----------
+ .. [1] Borgatti, S.P. and Halgin, D. In press. Analyzing Affiliation
+ Networks. In Carrington, P. and Scott, J. (eds) The Sage Handbook
+ of Social Network Analysis. Sage Publications.
+
+ """
+ if B.is_directed():
+ pred = B.pred
+ G = nx.DiGraph()
+ else:
+ pred = B.adj
+ G = nx.Graph()
+ G.graph.update(B.graph)
+ G.add_nodes_from((n, B.nodes[n]) for n in nodes)
+ for u in nodes:
+ unbrs = set(B[u])
+ nbrs2 = {n for nbr in unbrs for n in B[nbr]} - {u}
+ for v in nbrs2:
+ vnbrs = set(pred[v])
+ if jaccard:
+ wt = len(unbrs & vnbrs) / len(unbrs | vnbrs)
+ else:
+ wt = len(unbrs & vnbrs) / min(len(unbrs), len(vnbrs))
+ G.add_edge(u, v, weight=wt)
+ return G
+
+
+@not_implemented_for("multigraph")
+@nx._dispatchable(graphs="B", preserve_all_attrs=True, returns_graph=True)
+def generic_weighted_projected_graph(B, nodes, weight_function=None):
+ r"""Weighted projection of B with a user-specified weight function.
+
+ The bipartite network B is projected on to the specified nodes
+ with weights computed by a user-specified function. This function
+ must accept as a parameter the neighborhood sets of two nodes and
+ return an integer or a float.
+
+ The nodes retain their attributes and are connected in the resulting graph
+ if they have an edge to a common node in the original graph.
+
+ Parameters
+ ----------
+ B : NetworkX graph
+ The input graph should be bipartite.
+
+ nodes : list or iterable
+ Nodes to project onto (the "bottom" nodes).
+
+ weight_function : function
+ This function must accept as parameters the same input graph
+ that this function, and two nodes; and return an integer or a float.
+ The default function computes the number of shared neighbors.
+
+ Returns
+ -------
+ Graph : NetworkX graph
+ A graph that is the projection onto the given nodes.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> # Define some custom weight functions
+ >>> def jaccard(G, u, v):
+ ... unbrs = set(G[u])
+ ... vnbrs = set(G[v])
+ ... return float(len(unbrs & vnbrs)) / len(unbrs | vnbrs)
+ >>> def my_weight(G, u, v, weight="weight"):
+ ... w = 0
+ ... for nbr in set(G[u]) & set(G[v]):
+ ... w += G[u][nbr].get(weight, 1) + G[v][nbr].get(weight, 1)
+ ... return w
+ >>> # A complete bipartite graph with 4 nodes and 4 edges
+ >>> B = nx.complete_bipartite_graph(2, 2)
+ >>> # Add some arbitrary weight to the edges
+ >>> for i, (u, v) in enumerate(B.edges()):
+ ... B.edges[u, v]["weight"] = i + 1
+ >>> for edge in B.edges(data=True):
+ ... print(edge)
+ (0, 2, {'weight': 1})
+ (0, 3, {'weight': 2})
+ (1, 2, {'weight': 3})
+ (1, 3, {'weight': 4})
+ >>> # By default, the weight is the number of shared neighbors
+ >>> G = bipartite.generic_weighted_projected_graph(B, [0, 1])
+ >>> print(list(G.edges(data=True)))
+ [(0, 1, {'weight': 2})]
+ >>> # To specify a custom weight function use the weight_function parameter
+ >>> G = bipartite.generic_weighted_projected_graph(
+ ... B, [0, 1], weight_function=jaccard
+ ... )
+ >>> print(list(G.edges(data=True)))
+ [(0, 1, {'weight': 1.0})]
+ >>> G = bipartite.generic_weighted_projected_graph(
+ ... B, [0, 1], weight_function=my_weight
+ ... )
+ >>> print(list(G.edges(data=True)))
+ [(0, 1, {'weight': 10})]
+
+ Notes
+ -----
+ No attempt is made to verify that the input graph B is bipartite.
+ The graph and node properties are (shallow) copied to the projected graph.
+
+ See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
+ for further details on how bipartite graphs are handled in NetworkX.
+
+ See Also
+ --------
+ is_bipartite,
+ is_bipartite_node_set,
+ sets,
+ weighted_projected_graph,
+ collaboration_weighted_projected_graph,
+ overlap_weighted_projected_graph,
+ projected_graph
+
+ """
+ if B.is_directed():
+ pred = B.pred
+ G = nx.DiGraph()
+ else:
+ pred = B.adj
+ G = nx.Graph()
+ if weight_function is None:
+
+ def weight_function(G, u, v):
+ # Notice that we use set(pred[v]) for handling the directed case.
+ return len(set(G[u]) & set(pred[v]))
+
+ G.graph.update(B.graph)
+ G.add_nodes_from((n, B.nodes[n]) for n in nodes)
+ for u in nodes:
+ nbrs2 = {n for nbr in set(B[u]) for n in B[nbr]} - {u}
+ for v in nbrs2:
+ weight = weight_function(B, u, v)
+ G.add_edge(u, v, weight=weight)
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/redundancy.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/redundancy.py
new file mode 100644
index 00000000..b622b975
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/redundancy.py
@@ -0,0 +1,112 @@
+"""Node redundancy for bipartite graphs."""
+
+from itertools import combinations
+
+import networkx as nx
+from networkx import NetworkXError
+
+__all__ = ["node_redundancy"]
+
+
+@nx._dispatchable
+def node_redundancy(G, nodes=None):
+ r"""Computes the node redundancy coefficients for the nodes in the bipartite
+ graph `G`.
+
+ The redundancy coefficient of a node `v` is the fraction of pairs of
+ neighbors of `v` that are both linked to other nodes. In a one-mode
+ projection these nodes would be linked together even if `v` were
+ not there.
+
+ More formally, for any vertex `v`, the *redundancy coefficient of `v`* is
+ defined by
+
+ .. math::
+
+ rc(v) = \frac{|\{\{u, w\} \subseteq N(v),
+ \: \exists v' \neq v,\: (v',u) \in E\:
+ \mathrm{and}\: (v',w) \in E\}|}{ \frac{|N(v)|(|N(v)|-1)}{2}},
+
+ where `N(v)` is the set of neighbors of `v` in `G`.
+
+ Parameters
+ ----------
+ G : graph
+ A bipartite graph
+
+ nodes : list or iterable (optional)
+ Compute redundancy for these nodes. The default is all nodes in G.
+
+ Returns
+ -------
+ redundancy : dictionary
+ A dictionary keyed by node with the node redundancy value.
+
+ Examples
+ --------
+ Compute the redundancy coefficient of each node in a graph::
+
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.cycle_graph(4)
+ >>> rc = bipartite.node_redundancy(G)
+ >>> rc[0]
+ 1.0
+
+ Compute the average redundancy for the graph::
+
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.cycle_graph(4)
+ >>> rc = bipartite.node_redundancy(G)
+ >>> sum(rc.values()) / len(G)
+ 1.0
+
+ Compute the average redundancy for a set of nodes::
+
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.cycle_graph(4)
+ >>> rc = bipartite.node_redundancy(G)
+ >>> nodes = [0, 2]
+ >>> sum(rc[n] for n in nodes) / len(nodes)
+ 1.0
+
+ Raises
+ ------
+ NetworkXError
+ If any of the nodes in the graph (or in `nodes`, if specified) has
+ (out-)degree less than two (which would result in division by zero,
+ according to the definition of the redundancy coefficient).
+
+ References
+ ----------
+ .. [1] Latapy, Matthieu, Clémence Magnien, and Nathalie Del Vecchio (2008).
+ Basic notions for the analysis of large two-mode networks.
+ Social Networks 30(1), 31--48.
+
+ """
+ if nodes is None:
+ nodes = G
+ if any(len(G[v]) < 2 for v in nodes):
+ raise NetworkXError(
+ "Cannot compute redundancy coefficient for a node"
+ " that has fewer than two neighbors."
+ )
+ # TODO This can be trivially parallelized.
+ return {v: _node_redundancy(G, v) for v in nodes}
+
+
+def _node_redundancy(G, v):
+ """Returns the redundancy of the node `v` in the bipartite graph `G`.
+
+ If `G` is a graph with `n` nodes, the redundancy of a node is the ratio
+ of the "overlap" of `v` to the maximum possible overlap of `v`
+ according to its degree. The overlap of `v` is the number of pairs of
+ neighbors that have mutual neighbors themselves, other than `v`.
+
+ `v` must have at least two neighbors in `G`.
+
+ """
+ n = len(G[v])
+ overlap = sum(
+ 1 for (u, w) in combinations(G[v], 2) if (set(G[u]) & set(G[w])) - {v}
+ )
+ return (2 * overlap) / (n * (n - 1))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/spectral.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/spectral.py
new file mode 100644
index 00000000..cb9388f6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/spectral.py
@@ -0,0 +1,69 @@
+"""
+Spectral bipartivity measure.
+"""
+
+import networkx as nx
+
+__all__ = ["spectral_bipartivity"]
+
+
+@nx._dispatchable(edge_attrs="weight")
+def spectral_bipartivity(G, nodes=None, weight="weight"):
+ """Returns the spectral bipartivity.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ nodes : list or container optional(default is all nodes)
+ Nodes to return value of spectral bipartivity contribution.
+
+ weight : string or None optional (default = 'weight')
+ Edge data key to use for edge weights. If None, weights set to 1.
+
+ Returns
+ -------
+ sb : float or dict
+ A single number if the keyword nodes is not specified, or
+ a dictionary keyed by node with the spectral bipartivity contribution
+ of that node as the value.
+
+ Examples
+ --------
+ >>> from networkx.algorithms import bipartite
+ >>> G = nx.path_graph(4)
+ >>> bipartite.spectral_bipartivity(G)
+ 1.0
+
+ Notes
+ -----
+ This implementation uses Numpy (dense) matrices which are not efficient
+ for storing large sparse graphs.
+
+ See Also
+ --------
+ color
+
+ References
+ ----------
+ .. [1] E. Estrada and J. A. Rodríguez-Velázquez, "Spectral measures of
+ bipartivity in complex networks", PhysRev E 72, 046105 (2005)
+ """
+ import scipy as sp
+
+ nodelist = list(G) # ordering of nodes in matrix
+ A = nx.to_numpy_array(G, nodelist, weight=weight)
+ expA = sp.linalg.expm(A)
+ expmA = sp.linalg.expm(-A)
+ coshA = 0.5 * (expA + expmA)
+ if nodes is None:
+ # return single number for entire graph
+ return float(coshA.diagonal().sum() / expA.diagonal().sum())
+ else:
+ # contribution for individual nodes
+ index = dict(zip(nodelist, range(len(nodelist))))
+ sb = {}
+ for n in nodes:
+ i = index[n]
+ sb[n] = coshA.item(i, i) / expA.item(i, i)
+ return sb
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_basic.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_basic.py
new file mode 100644
index 00000000..655506b4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_basic.py
@@ -0,0 +1,125 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms import bipartite
+
+
+class TestBipartiteBasic:
+ def test_is_bipartite(self):
+ assert bipartite.is_bipartite(nx.path_graph(4))
+ assert bipartite.is_bipartite(nx.DiGraph([(1, 0)]))
+ assert not bipartite.is_bipartite(nx.complete_graph(3))
+
+ def test_bipartite_color(self):
+ G = nx.path_graph(4)
+ c = bipartite.color(G)
+ assert c == {0: 1, 1: 0, 2: 1, 3: 0}
+
+ def test_not_bipartite_color(self):
+ with pytest.raises(nx.NetworkXError):
+ c = bipartite.color(nx.complete_graph(4))
+
+ def test_bipartite_directed(self):
+ G = bipartite.random_graph(10, 10, 0.1, directed=True)
+ assert bipartite.is_bipartite(G)
+
+ def test_bipartite_sets(self):
+ G = nx.path_graph(4)
+ X, Y = bipartite.sets(G)
+ assert X == {0, 2}
+ assert Y == {1, 3}
+
+ def test_bipartite_sets_directed(self):
+ G = nx.path_graph(4)
+ D = G.to_directed()
+ X, Y = bipartite.sets(D)
+ assert X == {0, 2}
+ assert Y == {1, 3}
+
+ def test_bipartite_sets_given_top_nodes(self):
+ G = nx.path_graph(4)
+ top_nodes = [0, 2]
+ X, Y = bipartite.sets(G, top_nodes)
+ assert X == {0, 2}
+ assert Y == {1, 3}
+
+ def test_bipartite_sets_disconnected(self):
+ with pytest.raises(nx.AmbiguousSolution):
+ G = nx.path_graph(4)
+ G.add_edges_from([(5, 6), (6, 7)])
+ X, Y = bipartite.sets(G)
+
+ def test_is_bipartite_node_set(self):
+ G = nx.path_graph(4)
+
+ with pytest.raises(nx.AmbiguousSolution):
+ bipartite.is_bipartite_node_set(G, [1, 1, 2, 3])
+
+ assert bipartite.is_bipartite_node_set(G, [0, 2])
+ assert bipartite.is_bipartite_node_set(G, [1, 3])
+ assert not bipartite.is_bipartite_node_set(G, [1, 2])
+ G.add_edge(10, 20)
+ assert bipartite.is_bipartite_node_set(G, [0, 2, 10])
+ assert bipartite.is_bipartite_node_set(G, [0, 2, 20])
+ assert bipartite.is_bipartite_node_set(G, [1, 3, 10])
+ assert bipartite.is_bipartite_node_set(G, [1, 3, 20])
+
+ def test_bipartite_density(self):
+ G = nx.path_graph(5)
+ X, Y = bipartite.sets(G)
+ density = len(list(G.edges())) / (len(X) * len(Y))
+ assert bipartite.density(G, X) == density
+ D = nx.DiGraph(G.edges())
+ assert bipartite.density(D, X) == density / 2.0
+ assert bipartite.density(nx.Graph(), {}) == 0.0
+
+ def test_bipartite_degrees(self):
+ G = nx.path_graph(5)
+ X = {1, 3}
+ Y = {0, 2, 4}
+ u, d = bipartite.degrees(G, Y)
+ assert dict(u) == {1: 2, 3: 2}
+ assert dict(d) == {0: 1, 2: 2, 4: 1}
+
+ def test_bipartite_weighted_degrees(self):
+ G = nx.path_graph(5)
+ G.add_edge(0, 1, weight=0.1, other=0.2)
+ X = {1, 3}
+ Y = {0, 2, 4}
+ u, d = bipartite.degrees(G, Y, weight="weight")
+ assert dict(u) == {1: 1.1, 3: 2}
+ assert dict(d) == {0: 0.1, 2: 2, 4: 1}
+ u, d = bipartite.degrees(G, Y, weight="other")
+ assert dict(u) == {1: 1.2, 3: 2}
+ assert dict(d) == {0: 0.2, 2: 2, 4: 1}
+
+ def test_biadjacency_matrix_weight(self):
+ pytest.importorskip("scipy")
+ G = nx.path_graph(5)
+ G.add_edge(0, 1, weight=2, other=4)
+ X = [1, 3]
+ Y = [0, 2, 4]
+ M = bipartite.biadjacency_matrix(G, X, weight="weight")
+ assert M[0, 0] == 2
+ M = bipartite.biadjacency_matrix(G, X, weight="other")
+ assert M[0, 0] == 4
+
+ def test_biadjacency_matrix(self):
+ pytest.importorskip("scipy")
+ tops = [2, 5, 10]
+ bots = [5, 10, 15]
+ for i in range(len(tops)):
+ G = bipartite.random_graph(tops[i], bots[i], 0.2)
+ top = [n for n, d in G.nodes(data=True) if d["bipartite"] == 0]
+ M = bipartite.biadjacency_matrix(G, top)
+ assert M.shape[0] == tops[i]
+ assert M.shape[1] == bots[i]
+
+ def test_biadjacency_matrix_order(self):
+ pytest.importorskip("scipy")
+ G = nx.path_graph(5)
+ G.add_edge(0, 1, weight=2)
+ X = [3, 1]
+ Y = [4, 2, 0]
+ M = bipartite.biadjacency_matrix(G, X, Y, weight="weight")
+ assert M[1, 2] == 2
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_centrality.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_centrality.py
new file mode 100644
index 00000000..19fb5d11
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_centrality.py
@@ -0,0 +1,192 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms import bipartite
+
+
+class TestBipartiteCentrality:
+ @classmethod
+ def setup_class(cls):
+ cls.P4 = nx.path_graph(4)
+ cls.K3 = nx.complete_bipartite_graph(3, 3)
+ cls.C4 = nx.cycle_graph(4)
+ cls.davis = nx.davis_southern_women_graph()
+ cls.top_nodes = [
+ n for n, d in cls.davis.nodes(data=True) if d["bipartite"] == 0
+ ]
+
+ def test_degree_centrality(self):
+ d = bipartite.degree_centrality(self.P4, [1, 3])
+ answer = {0: 0.5, 1: 1.0, 2: 1.0, 3: 0.5}
+ assert d == answer
+ d = bipartite.degree_centrality(self.K3, [0, 1, 2])
+ answer = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0, 5: 1.0}
+ assert d == answer
+ d = bipartite.degree_centrality(self.C4, [0, 2])
+ answer = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0}
+ assert d == answer
+
+ def test_betweenness_centrality(self):
+ c = bipartite.betweenness_centrality(self.P4, [1, 3])
+ answer = {0: 0.0, 1: 1.0, 2: 1.0, 3: 0.0}
+ assert c == answer
+ c = bipartite.betweenness_centrality(self.K3, [0, 1, 2])
+ answer = {0: 0.125, 1: 0.125, 2: 0.125, 3: 0.125, 4: 0.125, 5: 0.125}
+ assert c == answer
+ c = bipartite.betweenness_centrality(self.C4, [0, 2])
+ answer = {0: 0.25, 1: 0.25, 2: 0.25, 3: 0.25}
+ assert c == answer
+
+ def test_closeness_centrality(self):
+ c = bipartite.closeness_centrality(self.P4, [1, 3])
+ answer = {0: 2.0 / 3, 1: 1.0, 2: 1.0, 3: 2.0 / 3}
+ assert c == answer
+ c = bipartite.closeness_centrality(self.K3, [0, 1, 2])
+ answer = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0, 5: 1.0}
+ assert c == answer
+ c = bipartite.closeness_centrality(self.C4, [0, 2])
+ answer = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0}
+ assert c == answer
+ G = nx.Graph()
+ G.add_node(0)
+ G.add_node(1)
+ c = bipartite.closeness_centrality(G, [0])
+ assert c == {0: 0.0, 1: 0.0}
+ c = bipartite.closeness_centrality(G, [1])
+ assert c == {0: 0.0, 1: 0.0}
+
+ def test_bipartite_closeness_centrality_unconnected(self):
+ G = nx.complete_bipartite_graph(3, 3)
+ G.add_edge(6, 7)
+ c = bipartite.closeness_centrality(G, [0, 2, 4, 6], normalized=False)
+ answer = {
+ 0: 10.0 / 7,
+ 2: 10.0 / 7,
+ 4: 10.0 / 7,
+ 6: 10.0,
+ 1: 10.0 / 7,
+ 3: 10.0 / 7,
+ 5: 10.0 / 7,
+ 7: 10.0,
+ }
+ assert c == answer
+
+ def test_davis_degree_centrality(self):
+ G = self.davis
+ deg = bipartite.degree_centrality(G, self.top_nodes)
+ answer = {
+ "E8": 0.78,
+ "E9": 0.67,
+ "E7": 0.56,
+ "Nora Fayette": 0.57,
+ "Evelyn Jefferson": 0.57,
+ "Theresa Anderson": 0.57,
+ "E6": 0.44,
+ "Sylvia Avondale": 0.50,
+ "Laura Mandeville": 0.50,
+ "Brenda Rogers": 0.50,
+ "Katherina Rogers": 0.43,
+ "E5": 0.44,
+ "Helen Lloyd": 0.36,
+ "E3": 0.33,
+ "Ruth DeSand": 0.29,
+ "Verne Sanderson": 0.29,
+ "E12": 0.33,
+ "Myra Liddel": 0.29,
+ "E11": 0.22,
+ "Eleanor Nye": 0.29,
+ "Frances Anderson": 0.29,
+ "Pearl Oglethorpe": 0.21,
+ "E4": 0.22,
+ "Charlotte McDowd": 0.29,
+ "E10": 0.28,
+ "Olivia Carleton": 0.14,
+ "Flora Price": 0.14,
+ "E2": 0.17,
+ "E1": 0.17,
+ "Dorothy Murchison": 0.14,
+ "E13": 0.17,
+ "E14": 0.17,
+ }
+ for node, value in answer.items():
+ assert value == pytest.approx(deg[node], abs=1e-2)
+
+ def test_davis_betweenness_centrality(self):
+ G = self.davis
+ bet = bipartite.betweenness_centrality(G, self.top_nodes)
+ answer = {
+ "E8": 0.24,
+ "E9": 0.23,
+ "E7": 0.13,
+ "Nora Fayette": 0.11,
+ "Evelyn Jefferson": 0.10,
+ "Theresa Anderson": 0.09,
+ "E6": 0.07,
+ "Sylvia Avondale": 0.07,
+ "Laura Mandeville": 0.05,
+ "Brenda Rogers": 0.05,
+ "Katherina Rogers": 0.05,
+ "E5": 0.04,
+ "Helen Lloyd": 0.04,
+ "E3": 0.02,
+ "Ruth DeSand": 0.02,
+ "Verne Sanderson": 0.02,
+ "E12": 0.02,
+ "Myra Liddel": 0.02,
+ "E11": 0.02,
+ "Eleanor Nye": 0.01,
+ "Frances Anderson": 0.01,
+ "Pearl Oglethorpe": 0.01,
+ "E4": 0.01,
+ "Charlotte McDowd": 0.01,
+ "E10": 0.01,
+ "Olivia Carleton": 0.01,
+ "Flora Price": 0.01,
+ "E2": 0.00,
+ "E1": 0.00,
+ "Dorothy Murchison": 0.00,
+ "E13": 0.00,
+ "E14": 0.00,
+ }
+ for node, value in answer.items():
+ assert value == pytest.approx(bet[node], abs=1e-2)
+
+ def test_davis_closeness_centrality(self):
+ G = self.davis
+ clos = bipartite.closeness_centrality(G, self.top_nodes)
+ answer = {
+ "E8": 0.85,
+ "E9": 0.79,
+ "E7": 0.73,
+ "Nora Fayette": 0.80,
+ "Evelyn Jefferson": 0.80,
+ "Theresa Anderson": 0.80,
+ "E6": 0.69,
+ "Sylvia Avondale": 0.77,
+ "Laura Mandeville": 0.73,
+ "Brenda Rogers": 0.73,
+ "Katherina Rogers": 0.73,
+ "E5": 0.59,
+ "Helen Lloyd": 0.73,
+ "E3": 0.56,
+ "Ruth DeSand": 0.71,
+ "Verne Sanderson": 0.71,
+ "E12": 0.56,
+ "Myra Liddel": 0.69,
+ "E11": 0.54,
+ "Eleanor Nye": 0.67,
+ "Frances Anderson": 0.67,
+ "Pearl Oglethorpe": 0.67,
+ "E4": 0.54,
+ "Charlotte McDowd": 0.60,
+ "E10": 0.55,
+ "Olivia Carleton": 0.59,
+ "Flora Price": 0.59,
+ "E2": 0.52,
+ "E1": 0.52,
+ "Dorothy Murchison": 0.65,
+ "E13": 0.52,
+ "E14": 0.52,
+ }
+ for node, value in answer.items():
+ assert value == pytest.approx(clos[node], abs=1e-2)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_cluster.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_cluster.py
new file mode 100644
index 00000000..72e2dbad
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_cluster.py
@@ -0,0 +1,84 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms import bipartite
+from networkx.algorithms.bipartite.cluster import cc_dot, cc_max, cc_min
+
+
+def test_pairwise_bipartite_cc_functions():
+ # Test functions for different kinds of bipartite clustering coefficients
+ # between pairs of nodes using 3 example graphs from figure 5 p. 40
+ # Latapy et al (2008)
+ G1 = nx.Graph([(0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (1, 5), (1, 6), (1, 7)])
+ G2 = nx.Graph([(0, 2), (0, 3), (0, 4), (1, 3), (1, 4), (1, 5)])
+ G3 = nx.Graph(
+ [(0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9)]
+ )
+ result = {
+ 0: [1 / 3.0, 2 / 3.0, 2 / 5.0],
+ 1: [1 / 2.0, 2 / 3.0, 2 / 3.0],
+ 2: [2 / 8.0, 2 / 5.0, 2 / 5.0],
+ }
+ for i, G in enumerate([G1, G2, G3]):
+ assert bipartite.is_bipartite(G)
+ assert cc_dot(set(G[0]), set(G[1])) == result[i][0]
+ assert cc_min(set(G[0]), set(G[1])) == result[i][1]
+ assert cc_max(set(G[0]), set(G[1])) == result[i][2]
+
+
+def test_star_graph():
+ G = nx.star_graph(3)
+ # all modes are the same
+ answer = {0: 0, 1: 1, 2: 1, 3: 1}
+ assert bipartite.clustering(G, mode="dot") == answer
+ assert bipartite.clustering(G, mode="min") == answer
+ assert bipartite.clustering(G, mode="max") == answer
+
+
+def test_not_bipartite():
+ with pytest.raises(nx.NetworkXError):
+ bipartite.clustering(nx.complete_graph(4))
+
+
+def test_bad_mode():
+ with pytest.raises(nx.NetworkXError):
+ bipartite.clustering(nx.path_graph(4), mode="foo")
+
+
+def test_path_graph():
+ G = nx.path_graph(4)
+ answer = {0: 0.5, 1: 0.5, 2: 0.5, 3: 0.5}
+ assert bipartite.clustering(G, mode="dot") == answer
+ assert bipartite.clustering(G, mode="max") == answer
+ answer = {0: 1, 1: 1, 2: 1, 3: 1}
+ assert bipartite.clustering(G, mode="min") == answer
+
+
+def test_average_path_graph():
+ G = nx.path_graph(4)
+ assert bipartite.average_clustering(G, mode="dot") == 0.5
+ assert bipartite.average_clustering(G, mode="max") == 0.5
+ assert bipartite.average_clustering(G, mode="min") == 1
+
+
+def test_ra_clustering_davis():
+ G = nx.davis_southern_women_graph()
+ cc4 = round(bipartite.robins_alexander_clustering(G), 3)
+ assert cc4 == 0.468
+
+
+def test_ra_clustering_square():
+ G = nx.path_graph(4)
+ G.add_edge(0, 3)
+ assert bipartite.robins_alexander_clustering(G) == 1.0
+
+
+def test_ra_clustering_zero():
+ G = nx.Graph()
+ assert bipartite.robins_alexander_clustering(G) == 0
+ G.add_nodes_from(range(4))
+ assert bipartite.robins_alexander_clustering(G) == 0
+ G.add_edges_from([(0, 1), (2, 3), (3, 4)])
+ assert bipartite.robins_alexander_clustering(G) == 0
+ G.add_edge(1, 2)
+ assert bipartite.robins_alexander_clustering(G) == 0
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_covering.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_covering.py
new file mode 100644
index 00000000..9507e134
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_covering.py
@@ -0,0 +1,33 @@
+import networkx as nx
+from networkx.algorithms import bipartite
+
+
+class TestMinEdgeCover:
+ """Tests for :func:`networkx.algorithms.bipartite.min_edge_cover`"""
+
+ def test_empty_graph(self):
+ G = nx.Graph()
+ assert bipartite.min_edge_cover(G) == set()
+
+ def test_graph_single_edge(self):
+ G = nx.Graph()
+ G.add_edge(0, 1)
+ assert bipartite.min_edge_cover(G) == {(0, 1), (1, 0)}
+
+ def test_bipartite_default(self):
+ G = nx.Graph()
+ G.add_nodes_from([1, 2, 3, 4], bipartite=0)
+ G.add_nodes_from(["a", "b", "c"], bipartite=1)
+ G.add_edges_from([(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c"), (4, "a")])
+ min_cover = bipartite.min_edge_cover(G)
+ assert nx.is_edge_cover(G, min_cover)
+ assert len(min_cover) == 8
+
+ def test_bipartite_explicit(self):
+ G = nx.Graph()
+ G.add_nodes_from([1, 2, 3, 4], bipartite=0)
+ G.add_nodes_from(["a", "b", "c"], bipartite=1)
+ G.add_edges_from([(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c"), (4, "a")])
+ min_cover = bipartite.min_edge_cover(G, bipartite.eppstein_matching)
+ assert nx.is_edge_cover(G, min_cover)
+ assert len(min_cover) == 8
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_edgelist.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_edgelist.py
new file mode 100644
index 00000000..66be8a2f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_edgelist.py
@@ -0,0 +1,240 @@
+"""
+Unit tests for bipartite edgelists.
+"""
+
+import io
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms import bipartite
+from networkx.utils import edges_equal, graphs_equal, nodes_equal
+
+
+class TestEdgelist:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.Graph(name="test")
+ e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")]
+ cls.G.add_edges_from(e)
+ cls.G.add_nodes_from(["a", "c", "e"], bipartite=0)
+ cls.G.add_nodes_from(["b", "d", "f"], bipartite=1)
+ cls.G.add_node("g", bipartite=0)
+ cls.DG = nx.DiGraph(cls.G)
+ cls.MG = nx.MultiGraph()
+ cls.MG.add_edges_from([(1, 2), (1, 2), (1, 2)])
+ cls.MG.add_node(1, bipartite=0)
+ cls.MG.add_node(2, bipartite=1)
+
+ def test_read_edgelist_1(self):
+ s = b"""\
+# comment line
+1 2
+# comment line
+2 3
+"""
+ bytesIO = io.BytesIO(s)
+ G = bipartite.read_edgelist(bytesIO, nodetype=int)
+ assert edges_equal(G.edges(), [(1, 2), (2, 3)])
+
+ def test_read_edgelist_3(self):
+ s = b"""\
+# comment line
+1 2 {'weight':2.0}
+# comment line
+2 3 {'weight':3.0}
+"""
+ bytesIO = io.BytesIO(s)
+ G = bipartite.read_edgelist(bytesIO, nodetype=int, data=False)
+ assert edges_equal(G.edges(), [(1, 2), (2, 3)])
+
+ bytesIO = io.BytesIO(s)
+ G = bipartite.read_edgelist(bytesIO, nodetype=int, data=True)
+ assert edges_equal(
+ G.edges(data=True), [(1, 2, {"weight": 2.0}), (2, 3, {"weight": 3.0})]
+ )
+
+ def test_write_edgelist_1(self):
+ fh = io.BytesIO()
+ G = nx.Graph()
+ G.add_edges_from([(1, 2), (2, 3)])
+ G.add_node(1, bipartite=0)
+ G.add_node(2, bipartite=1)
+ G.add_node(3, bipartite=0)
+ bipartite.write_edgelist(G, fh, data=False)
+ fh.seek(0)
+ assert fh.read() == b"1 2\n3 2\n"
+
+ def test_write_edgelist_2(self):
+ fh = io.BytesIO()
+ G = nx.Graph()
+ G.add_edges_from([(1, 2), (2, 3)])
+ G.add_node(1, bipartite=0)
+ G.add_node(2, bipartite=1)
+ G.add_node(3, bipartite=0)
+ bipartite.write_edgelist(G, fh, data=True)
+ fh.seek(0)
+ assert fh.read() == b"1 2 {}\n3 2 {}\n"
+
+ def test_write_edgelist_3(self):
+ fh = io.BytesIO()
+ G = nx.Graph()
+ G.add_edge(1, 2, weight=2.0)
+ G.add_edge(2, 3, weight=3.0)
+ G.add_node(1, bipartite=0)
+ G.add_node(2, bipartite=1)
+ G.add_node(3, bipartite=0)
+ bipartite.write_edgelist(G, fh, data=True)
+ fh.seek(0)
+ assert fh.read() == b"1 2 {'weight': 2.0}\n3 2 {'weight': 3.0}\n"
+
+ def test_write_edgelist_4(self):
+ fh = io.BytesIO()
+ G = nx.Graph()
+ G.add_edge(1, 2, weight=2.0)
+ G.add_edge(2, 3, weight=3.0)
+ G.add_node(1, bipartite=0)
+ G.add_node(2, bipartite=1)
+ G.add_node(3, bipartite=0)
+ bipartite.write_edgelist(G, fh, data=[("weight")])
+ fh.seek(0)
+ assert fh.read() == b"1 2 2.0\n3 2 3.0\n"
+
+ def test_unicode(self, tmp_path):
+ G = nx.Graph()
+ name1 = chr(2344) + chr(123) + chr(6543)
+ name2 = chr(5543) + chr(1543) + chr(324)
+ G.add_edge(name1, "Radiohead", **{name2: 3})
+ G.add_node(name1, bipartite=0)
+ G.add_node("Radiohead", bipartite=1)
+
+ fname = tmp_path / "edgelist.txt"
+ bipartite.write_edgelist(G, fname)
+ H = bipartite.read_edgelist(fname)
+ assert graphs_equal(G, H)
+
+ def test_latin1_issue(self, tmp_path):
+ G = nx.Graph()
+ name1 = chr(2344) + chr(123) + chr(6543)
+ name2 = chr(5543) + chr(1543) + chr(324)
+ G.add_edge(name1, "Radiohead", **{name2: 3})
+ G.add_node(name1, bipartite=0)
+ G.add_node("Radiohead", bipartite=1)
+
+ fname = tmp_path / "edgelist.txt"
+ with pytest.raises(UnicodeEncodeError):
+ bipartite.write_edgelist(G, fname, encoding="latin-1")
+
+ def test_latin1(self, tmp_path):
+ G = nx.Graph()
+ name1 = "Bj" + chr(246) + "rk"
+ name2 = chr(220) + "ber"
+ G.add_edge(name1, "Radiohead", **{name2: 3})
+ G.add_node(name1, bipartite=0)
+ G.add_node("Radiohead", bipartite=1)
+
+ fname = tmp_path / "edgelist.txt"
+ bipartite.write_edgelist(G, fname, encoding="latin-1")
+ H = bipartite.read_edgelist(fname, encoding="latin-1")
+ assert graphs_equal(G, H)
+
+ def test_edgelist_graph(self, tmp_path):
+ G = self.G
+ fname = tmp_path / "edgelist.txt"
+ bipartite.write_edgelist(G, fname)
+ H = bipartite.read_edgelist(fname)
+ H2 = bipartite.read_edgelist(fname)
+ assert H is not H2 # they should be different graphs
+ G.remove_node("g") # isolated nodes are not written in edgelist
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_edgelist_integers(self, tmp_path):
+ G = nx.convert_node_labels_to_integers(self.G)
+ fname = tmp_path / "edgelist.txt"
+ bipartite.write_edgelist(G, fname)
+ H = bipartite.read_edgelist(fname, nodetype=int)
+ # isolated nodes are not written in edgelist
+ G.remove_nodes_from(list(nx.isolates(G)))
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_edgelist_multigraph(self, tmp_path):
+ G = self.MG
+ fname = tmp_path / "edgelist.txt"
+ bipartite.write_edgelist(G, fname)
+ H = bipartite.read_edgelist(fname, nodetype=int, create_using=nx.MultiGraph())
+ H2 = bipartite.read_edgelist(fname, nodetype=int, create_using=nx.MultiGraph())
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_empty_digraph(self):
+ with pytest.raises(nx.NetworkXNotImplemented):
+ bytesIO = io.BytesIO()
+ bipartite.write_edgelist(nx.DiGraph(), bytesIO)
+
+ def test_raise_attribute(self):
+ with pytest.raises(AttributeError):
+ G = nx.path_graph(4)
+ bytesIO = io.BytesIO()
+ bipartite.write_edgelist(G, bytesIO)
+
+ def test_parse_edgelist(self):
+ """Tests for conditions specific to
+ parse_edge_list method"""
+
+ # ignore strings of length less than 2
+ lines = ["1 2", "2 3", "3 1", "4", " "]
+ G = bipartite.parse_edgelist(lines, nodetype=int)
+ assert list(G.nodes) == [1, 2, 3]
+
+ # Exception raised when node is not convertible
+ # to specified data type
+ with pytest.raises(TypeError, match=".*Failed to convert nodes"):
+ lines = ["a b", "b c", "c a"]
+ G = bipartite.parse_edgelist(lines, nodetype=int)
+
+ # Exception raised when format of data is not
+ # convertible to dictionary object
+ with pytest.raises(TypeError, match=".*Failed to convert edge data"):
+ lines = ["1 2 3", "2 3 4", "3 1 2"]
+ G = bipartite.parse_edgelist(lines, nodetype=int)
+
+ # Exception raised when edge data and data
+ # keys are not of same length
+ with pytest.raises(IndexError):
+ lines = ["1 2 3 4", "2 3 4"]
+ G = bipartite.parse_edgelist(
+ lines, nodetype=int, data=[("weight", int), ("key", int)]
+ )
+
+ # Exception raised when edge data is not
+ # convertible to specified data type
+ with pytest.raises(TypeError, match=".*Failed to convert key data"):
+ lines = ["1 2 3 a", "2 3 4 b"]
+ G = bipartite.parse_edgelist(
+ lines, nodetype=int, data=[("weight", int), ("key", int)]
+ )
+
+
+def test_bipartite_edgelist_consistent_strip_handling():
+ """See gh-7462
+
+ Input when printed looks like:
+
+ A B interaction 2
+ B C interaction 4
+ C A interaction
+
+ Note the trailing \\t in the last line, which indicates the existence of
+ an empty data field.
+ """
+ lines = io.StringIO(
+ "A\tB\tinteraction\t2\nB\tC\tinteraction\t4\nC\tA\tinteraction\t"
+ )
+ descr = [("type", str), ("weight", str)]
+ # Should not raise
+ G = nx.bipartite.parse_edgelist(lines, delimiter="\t", data=descr)
+ expected = [("A", "B", "2"), ("A", "C", ""), ("B", "C", "4")]
+ assert sorted(G.edges(data="weight")) == expected
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_extendability.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_extendability.py
new file mode 100644
index 00000000..17b71243
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_extendability.py
@@ -0,0 +1,334 @@
+import pytest
+
+import networkx as nx
+
+
+def test_selfloops_raises():
+ G = nx.ladder_graph(3)
+ G.add_edge(0, 0)
+ with pytest.raises(nx.NetworkXError, match=".*not bipartite"):
+ nx.bipartite.maximal_extendability(G)
+
+
+def test_disconnected_raises():
+ G = nx.ladder_graph(3)
+ G.add_node("a")
+ with pytest.raises(nx.NetworkXError, match=".*not connected"):
+ nx.bipartite.maximal_extendability(G)
+
+
+def test_not_bipartite_raises():
+ G = nx.complete_graph(5)
+ with pytest.raises(nx.NetworkXError, match=".*not bipartite"):
+ nx.bipartite.maximal_extendability(G)
+
+
+def test_no_perfect_matching_raises():
+ G = nx.Graph([(0, 1), (0, 2)])
+ with pytest.raises(nx.NetworkXError, match=".*not contain a perfect matching"):
+ nx.bipartite.maximal_extendability(G)
+
+
+def test_residual_graph_not_strongly_connected_raises():
+ G = nx.Graph([(1, 2), (2, 3), (3, 4)])
+ with pytest.raises(
+ nx.NetworkXError, match="The residual graph of G is not strongly connected"
+ ):
+ nx.bipartite.maximal_extendability(G)
+
+
+def test_ladder_graph_is_1():
+ G = nx.ladder_graph(3)
+ assert nx.bipartite.maximal_extendability(G) == 1
+
+
+def test_cubical_graph_is_2():
+ G = nx.cubical_graph()
+ assert nx.bipartite.maximal_extendability(G) == 2
+
+
+def test_k_is_3():
+ G = nx.Graph(
+ [
+ (1, 6),
+ (1, 7),
+ (1, 8),
+ (1, 9),
+ (2, 6),
+ (2, 7),
+ (2, 8),
+ (2, 10),
+ (3, 6),
+ (3, 8),
+ (3, 9),
+ (3, 10),
+ (4, 7),
+ (4, 8),
+ (4, 9),
+ (4, 10),
+ (5, 6),
+ (5, 7),
+ (5, 9),
+ (5, 10),
+ ]
+ )
+ assert nx.bipartite.maximal_extendability(G) == 3
+
+
+def test_k_is_4():
+ G = nx.Graph(
+ [
+ (8, 1),
+ (8, 2),
+ (8, 3),
+ (8, 4),
+ (8, 5),
+ (9, 1),
+ (9, 2),
+ (9, 3),
+ (9, 4),
+ (9, 7),
+ (10, 1),
+ (10, 2),
+ (10, 3),
+ (10, 4),
+ (10, 6),
+ (11, 1),
+ (11, 2),
+ (11, 5),
+ (11, 6),
+ (11, 7),
+ (12, 1),
+ (12, 3),
+ (12, 5),
+ (12, 6),
+ (12, 7),
+ (13, 2),
+ (13, 4),
+ (13, 5),
+ (13, 6),
+ (13, 7),
+ (14, 3),
+ (14, 4),
+ (14, 5),
+ (14, 6),
+ (14, 7),
+ ]
+ )
+ assert nx.bipartite.maximal_extendability(G) == 4
+
+
+def test_k_is_5():
+ G = nx.Graph(
+ [
+ (8, 1),
+ (8, 2),
+ (8, 3),
+ (8, 4),
+ (8, 5),
+ (8, 6),
+ (9, 1),
+ (9, 2),
+ (9, 3),
+ (9, 4),
+ (9, 5),
+ (9, 7),
+ (10, 1),
+ (10, 2),
+ (10, 3),
+ (10, 4),
+ (10, 6),
+ (10, 7),
+ (11, 1),
+ (11, 2),
+ (11, 3),
+ (11, 5),
+ (11, 6),
+ (11, 7),
+ (12, 1),
+ (12, 2),
+ (12, 4),
+ (12, 5),
+ (12, 6),
+ (12, 7),
+ (13, 1),
+ (13, 3),
+ (13, 4),
+ (13, 5),
+ (13, 6),
+ (13, 7),
+ (14, 2),
+ (14, 3),
+ (14, 4),
+ (14, 5),
+ (14, 6),
+ (14, 7),
+ ]
+ )
+ assert nx.bipartite.maximal_extendability(G) == 5
+
+
+def test_k_is_6():
+ G = nx.Graph(
+ [
+ (9, 1),
+ (9, 2),
+ (9, 3),
+ (9, 4),
+ (9, 5),
+ (9, 6),
+ (9, 7),
+ (10, 1),
+ (10, 2),
+ (10, 3),
+ (10, 4),
+ (10, 5),
+ (10, 6),
+ (10, 8),
+ (11, 1),
+ (11, 2),
+ (11, 3),
+ (11, 4),
+ (11, 5),
+ (11, 7),
+ (11, 8),
+ (12, 1),
+ (12, 2),
+ (12, 3),
+ (12, 4),
+ (12, 6),
+ (12, 7),
+ (12, 8),
+ (13, 1),
+ (13, 2),
+ (13, 3),
+ (13, 5),
+ (13, 6),
+ (13, 7),
+ (13, 8),
+ (14, 1),
+ (14, 2),
+ (14, 4),
+ (14, 5),
+ (14, 6),
+ (14, 7),
+ (14, 8),
+ (15, 1),
+ (15, 3),
+ (15, 4),
+ (15, 5),
+ (15, 6),
+ (15, 7),
+ (15, 8),
+ (16, 2),
+ (16, 3),
+ (16, 4),
+ (16, 5),
+ (16, 6),
+ (16, 7),
+ (16, 8),
+ ]
+ )
+ assert nx.bipartite.maximal_extendability(G) == 6
+
+
+def test_k_is_7():
+ G = nx.Graph(
+ [
+ (1, 11),
+ (1, 12),
+ (1, 13),
+ (1, 14),
+ (1, 15),
+ (1, 16),
+ (1, 17),
+ (1, 18),
+ (2, 11),
+ (2, 12),
+ (2, 13),
+ (2, 14),
+ (2, 15),
+ (2, 16),
+ (2, 17),
+ (2, 19),
+ (3, 11),
+ (3, 12),
+ (3, 13),
+ (3, 14),
+ (3, 15),
+ (3, 16),
+ (3, 17),
+ (3, 20),
+ (4, 11),
+ (4, 12),
+ (4, 13),
+ (4, 14),
+ (4, 15),
+ (4, 16),
+ (4, 17),
+ (4, 18),
+ (4, 19),
+ (4, 20),
+ (5, 11),
+ (5, 12),
+ (5, 13),
+ (5, 14),
+ (5, 15),
+ (5, 16),
+ (5, 17),
+ (5, 18),
+ (5, 19),
+ (5, 20),
+ (6, 11),
+ (6, 12),
+ (6, 13),
+ (6, 14),
+ (6, 15),
+ (6, 16),
+ (6, 17),
+ (6, 18),
+ (6, 19),
+ (6, 20),
+ (7, 11),
+ (7, 12),
+ (7, 13),
+ (7, 14),
+ (7, 15),
+ (7, 16),
+ (7, 17),
+ (7, 18),
+ (7, 19),
+ (7, 20),
+ (8, 11),
+ (8, 12),
+ (8, 13),
+ (8, 14),
+ (8, 15),
+ (8, 16),
+ (8, 17),
+ (8, 18),
+ (8, 19),
+ (8, 20),
+ (9, 11),
+ (9, 12),
+ (9, 13),
+ (9, 14),
+ (9, 15),
+ (9, 16),
+ (9, 17),
+ (9, 18),
+ (9, 19),
+ (9, 20),
+ (10, 11),
+ (10, 12),
+ (10, 13),
+ (10, 14),
+ (10, 15),
+ (10, 16),
+ (10, 17),
+ (10, 18),
+ (10, 19),
+ (10, 20),
+ ]
+ )
+ assert nx.bipartite.maximal_extendability(G) == 7
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_generators.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_generators.py
new file mode 100644
index 00000000..8b1e7793
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_generators.py
@@ -0,0 +1,409 @@
+import numbers
+
+import pytest
+
+import networkx as nx
+
+from ..generators import (
+ alternating_havel_hakimi_graph,
+ complete_bipartite_graph,
+ configuration_model,
+ gnmk_random_graph,
+ havel_hakimi_graph,
+ preferential_attachment_graph,
+ random_graph,
+ reverse_havel_hakimi_graph,
+)
+
+"""
+Generators - Bipartite
+----------------------
+"""
+
+
+class TestGeneratorsBipartite:
+ def test_complete_bipartite_graph(self):
+ G = complete_bipartite_graph(0, 0)
+ assert nx.is_isomorphic(G, nx.null_graph())
+
+ for i in [1, 5]:
+ G = complete_bipartite_graph(i, 0)
+ assert nx.is_isomorphic(G, nx.empty_graph(i))
+ G = complete_bipartite_graph(0, i)
+ assert nx.is_isomorphic(G, nx.empty_graph(i))
+
+ G = complete_bipartite_graph(2, 2)
+ assert nx.is_isomorphic(G, nx.cycle_graph(4))
+
+ G = complete_bipartite_graph(1, 5)
+ assert nx.is_isomorphic(G, nx.star_graph(5))
+
+ G = complete_bipartite_graph(5, 1)
+ assert nx.is_isomorphic(G, nx.star_graph(5))
+
+ # complete_bipartite_graph(m1,m2) is a connected graph with
+ # m1+m2 nodes and m1*m2 edges
+ for m1, m2 in [(5, 11), (7, 3)]:
+ G = complete_bipartite_graph(m1, m2)
+ assert nx.number_of_nodes(G) == m1 + m2
+ assert nx.number_of_edges(G) == m1 * m2
+
+ with pytest.raises(nx.NetworkXError):
+ complete_bipartite_graph(7, 3, create_using=nx.DiGraph)
+ with pytest.raises(nx.NetworkXError):
+ complete_bipartite_graph(7, 3, create_using=nx.MultiDiGraph)
+
+ mG = complete_bipartite_graph(7, 3, create_using=nx.MultiGraph)
+ assert mG.is_multigraph()
+ assert sorted(mG.edges()) == sorted(G.edges())
+
+ mG = complete_bipartite_graph(7, 3, create_using=nx.MultiGraph)
+ assert mG.is_multigraph()
+ assert sorted(mG.edges()) == sorted(G.edges())
+
+ mG = complete_bipartite_graph(7, 3) # default to Graph
+ assert sorted(mG.edges()) == sorted(G.edges())
+ assert not mG.is_multigraph()
+ assert not mG.is_directed()
+
+ # specify nodes rather than number of nodes
+ for n1, n2 in [([1, 2], "ab"), (3, 2), (3, "ab"), ("ab", 3)]:
+ G = complete_bipartite_graph(n1, n2)
+ if isinstance(n1, numbers.Integral):
+ if isinstance(n2, numbers.Integral):
+ n2 = range(n1, n1 + n2)
+ n1 = range(n1)
+ elif isinstance(n2, numbers.Integral):
+ n2 = range(n2)
+ edges = {(u, v) for u in n1 for v in n2}
+ assert edges == set(G.edges)
+ assert G.size() == len(edges)
+
+ # raise when node sets are not distinct
+ for n1, n2 in [([1, 2], 3), (3, [1, 2]), ("abc", "bcd")]:
+ pytest.raises(nx.NetworkXError, complete_bipartite_graph, n1, n2)
+
+ def test_configuration_model(self):
+ aseq = []
+ bseq = []
+ G = configuration_model(aseq, bseq)
+ assert len(G) == 0
+
+ aseq = [0, 0]
+ bseq = [0, 0]
+ G = configuration_model(aseq, bseq)
+ assert len(G) == 4
+ assert G.number_of_edges() == 0
+
+ aseq = [3, 3, 3, 3]
+ bseq = [2, 2, 2, 2, 2]
+ pytest.raises(nx.NetworkXError, configuration_model, aseq, bseq)
+
+ aseq = [3, 3, 3, 3]
+ bseq = [2, 2, 2, 2, 2, 2]
+ G = configuration_model(aseq, bseq)
+ assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
+
+ aseq = [2, 2, 2, 2, 2, 2]
+ bseq = [3, 3, 3, 3]
+ G = configuration_model(aseq, bseq)
+ assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
+
+ aseq = [2, 2, 2, 1, 1, 1]
+ bseq = [3, 3, 3]
+ G = configuration_model(aseq, bseq)
+ assert G.is_multigraph()
+ assert not G.is_directed()
+ assert sorted(d for n, d in G.degree()) == [1, 1, 1, 2, 2, 2, 3, 3, 3]
+
+ GU = nx.projected_graph(nx.Graph(G), range(len(aseq)))
+ assert GU.number_of_nodes() == 6
+
+ GD = nx.projected_graph(nx.Graph(G), range(len(aseq), len(aseq) + len(bseq)))
+ assert GD.number_of_nodes() == 3
+
+ G = reverse_havel_hakimi_graph(aseq, bseq, create_using=nx.Graph)
+ assert not G.is_multigraph()
+ assert not G.is_directed()
+
+ pytest.raises(
+ nx.NetworkXError, configuration_model, aseq, bseq, create_using=nx.DiGraph()
+ )
+ pytest.raises(
+ nx.NetworkXError, configuration_model, aseq, bseq, create_using=nx.DiGraph
+ )
+ pytest.raises(
+ nx.NetworkXError,
+ configuration_model,
+ aseq,
+ bseq,
+ create_using=nx.MultiDiGraph,
+ )
+
+ def test_havel_hakimi_graph(self):
+ aseq = []
+ bseq = []
+ G = havel_hakimi_graph(aseq, bseq)
+ assert len(G) == 0
+
+ aseq = [0, 0]
+ bseq = [0, 0]
+ G = havel_hakimi_graph(aseq, bseq)
+ assert len(G) == 4
+ assert G.number_of_edges() == 0
+
+ aseq = [3, 3, 3, 3]
+ bseq = [2, 2, 2, 2, 2]
+ pytest.raises(nx.NetworkXError, havel_hakimi_graph, aseq, bseq)
+
+ bseq = [2, 2, 2, 2, 2, 2]
+ G = havel_hakimi_graph(aseq, bseq)
+ assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
+
+ aseq = [2, 2, 2, 2, 2, 2]
+ bseq = [3, 3, 3, 3]
+ G = havel_hakimi_graph(aseq, bseq)
+ assert G.is_multigraph()
+ assert not G.is_directed()
+ assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
+
+ GU = nx.projected_graph(nx.Graph(G), range(len(aseq)))
+ assert GU.number_of_nodes() == 6
+
+ GD = nx.projected_graph(nx.Graph(G), range(len(aseq), len(aseq) + len(bseq)))
+ assert GD.number_of_nodes() == 4
+
+ G = reverse_havel_hakimi_graph(aseq, bseq, create_using=nx.Graph)
+ assert not G.is_multigraph()
+ assert not G.is_directed()
+
+ pytest.raises(
+ nx.NetworkXError, havel_hakimi_graph, aseq, bseq, create_using=nx.DiGraph
+ )
+ pytest.raises(
+ nx.NetworkXError, havel_hakimi_graph, aseq, bseq, create_using=nx.DiGraph
+ )
+ pytest.raises(
+ nx.NetworkXError,
+ havel_hakimi_graph,
+ aseq,
+ bseq,
+ create_using=nx.MultiDiGraph,
+ )
+
+ def test_reverse_havel_hakimi_graph(self):
+ aseq = []
+ bseq = []
+ G = reverse_havel_hakimi_graph(aseq, bseq)
+ assert len(G) == 0
+
+ aseq = [0, 0]
+ bseq = [0, 0]
+ G = reverse_havel_hakimi_graph(aseq, bseq)
+ assert len(G) == 4
+ assert G.number_of_edges() == 0
+
+ aseq = [3, 3, 3, 3]
+ bseq = [2, 2, 2, 2, 2]
+ pytest.raises(nx.NetworkXError, reverse_havel_hakimi_graph, aseq, bseq)
+
+ bseq = [2, 2, 2, 2, 2, 2]
+ G = reverse_havel_hakimi_graph(aseq, bseq)
+ assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
+
+ aseq = [2, 2, 2, 2, 2, 2]
+ bseq = [3, 3, 3, 3]
+ G = reverse_havel_hakimi_graph(aseq, bseq)
+ assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
+
+ aseq = [2, 2, 2, 1, 1, 1]
+ bseq = [3, 3, 3]
+ G = reverse_havel_hakimi_graph(aseq, bseq)
+ assert G.is_multigraph()
+ assert not G.is_directed()
+ assert sorted(d for n, d in G.degree()) == [1, 1, 1, 2, 2, 2, 3, 3, 3]
+
+ GU = nx.projected_graph(nx.Graph(G), range(len(aseq)))
+ assert GU.number_of_nodes() == 6
+
+ GD = nx.projected_graph(nx.Graph(G), range(len(aseq), len(aseq) + len(bseq)))
+ assert GD.number_of_nodes() == 3
+
+ G = reverse_havel_hakimi_graph(aseq, bseq, create_using=nx.Graph)
+ assert not G.is_multigraph()
+ assert not G.is_directed()
+
+ pytest.raises(
+ nx.NetworkXError,
+ reverse_havel_hakimi_graph,
+ aseq,
+ bseq,
+ create_using=nx.DiGraph,
+ )
+ pytest.raises(
+ nx.NetworkXError,
+ reverse_havel_hakimi_graph,
+ aseq,
+ bseq,
+ create_using=nx.DiGraph,
+ )
+ pytest.raises(
+ nx.NetworkXError,
+ reverse_havel_hakimi_graph,
+ aseq,
+ bseq,
+ create_using=nx.MultiDiGraph,
+ )
+
+ def test_alternating_havel_hakimi_graph(self):
+ aseq = []
+ bseq = []
+ G = alternating_havel_hakimi_graph(aseq, bseq)
+ assert len(G) == 0
+
+ aseq = [0, 0]
+ bseq = [0, 0]
+ G = alternating_havel_hakimi_graph(aseq, bseq)
+ assert len(G) == 4
+ assert G.number_of_edges() == 0
+
+ aseq = [3, 3, 3, 3]
+ bseq = [2, 2, 2, 2, 2]
+ pytest.raises(nx.NetworkXError, alternating_havel_hakimi_graph, aseq, bseq)
+
+ bseq = [2, 2, 2, 2, 2, 2]
+ G = alternating_havel_hakimi_graph(aseq, bseq)
+ assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
+
+ aseq = [2, 2, 2, 2, 2, 2]
+ bseq = [3, 3, 3, 3]
+ G = alternating_havel_hakimi_graph(aseq, bseq)
+ assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
+
+ aseq = [2, 2, 2, 1, 1, 1]
+ bseq = [3, 3, 3]
+ G = alternating_havel_hakimi_graph(aseq, bseq)
+ assert G.is_multigraph()
+ assert not G.is_directed()
+ assert sorted(d for n, d in G.degree()) == [1, 1, 1, 2, 2, 2, 3, 3, 3]
+
+ GU = nx.projected_graph(nx.Graph(G), range(len(aseq)))
+ assert GU.number_of_nodes() == 6
+
+ GD = nx.projected_graph(nx.Graph(G), range(len(aseq), len(aseq) + len(bseq)))
+ assert GD.number_of_nodes() == 3
+
+ G = reverse_havel_hakimi_graph(aseq, bseq, create_using=nx.Graph)
+ assert not G.is_multigraph()
+ assert not G.is_directed()
+
+ pytest.raises(
+ nx.NetworkXError,
+ alternating_havel_hakimi_graph,
+ aseq,
+ bseq,
+ create_using=nx.DiGraph,
+ )
+ pytest.raises(
+ nx.NetworkXError,
+ alternating_havel_hakimi_graph,
+ aseq,
+ bseq,
+ create_using=nx.DiGraph,
+ )
+ pytest.raises(
+ nx.NetworkXError,
+ alternating_havel_hakimi_graph,
+ aseq,
+ bseq,
+ create_using=nx.MultiDiGraph,
+ )
+
+ def test_preferential_attachment(self):
+ aseq = [3, 2, 1, 1]
+ G = preferential_attachment_graph(aseq, 0.5)
+ assert G.is_multigraph()
+ assert not G.is_directed()
+
+ G = preferential_attachment_graph(aseq, 0.5, create_using=nx.Graph)
+ assert not G.is_multigraph()
+ assert not G.is_directed()
+
+ pytest.raises(
+ nx.NetworkXError,
+ preferential_attachment_graph,
+ aseq,
+ 0.5,
+ create_using=nx.DiGraph(),
+ )
+ pytest.raises(
+ nx.NetworkXError,
+ preferential_attachment_graph,
+ aseq,
+ 0.5,
+ create_using=nx.DiGraph(),
+ )
+ pytest.raises(
+ nx.NetworkXError,
+ preferential_attachment_graph,
+ aseq,
+ 0.5,
+ create_using=nx.DiGraph(),
+ )
+
+ def test_random_graph(self):
+ n = 10
+ m = 20
+ G = random_graph(n, m, 0.9)
+ assert len(G) == 30
+ assert nx.is_bipartite(G)
+ X, Y = nx.algorithms.bipartite.sets(G)
+ assert set(range(n)) == X
+ assert set(range(n, n + m)) == Y
+
+ def test_random_digraph(self):
+ n = 10
+ m = 20
+ G = random_graph(n, m, 0.9, directed=True)
+ assert len(G) == 30
+ assert nx.is_bipartite(G)
+ X, Y = nx.algorithms.bipartite.sets(G)
+ assert set(range(n)) == X
+ assert set(range(n, n + m)) == Y
+
+ def test_gnmk_random_graph(self):
+ n = 10
+ m = 20
+ edges = 100
+ # set seed because sometimes it is not connected
+ # which raises an error in bipartite.sets(G) below.
+ G = gnmk_random_graph(n, m, edges, seed=1234)
+ assert len(G) == n + m
+ assert nx.is_bipartite(G)
+ X, Y = nx.algorithms.bipartite.sets(G)
+ # print(X)
+ assert set(range(n)) == X
+ assert set(range(n, n + m)) == Y
+ assert edges == len(list(G.edges()))
+
+ def test_gnmk_random_graph_complete(self):
+ n = 10
+ m = 20
+ edges = 200
+ G = gnmk_random_graph(n, m, edges)
+ assert len(G) == n + m
+ assert nx.is_bipartite(G)
+ X, Y = nx.algorithms.bipartite.sets(G)
+ # print(X)
+ assert set(range(n)) == X
+ assert set(range(n, n + m)) == Y
+ assert edges == len(list(G.edges()))
+
+ @pytest.mark.parametrize("n", (4, range(4), {0, 1, 2, 3}))
+ @pytest.mark.parametrize("m", (range(4, 7), {4, 5, 6}))
+ def test_complete_bipartite_graph_str(self, n, m):
+ """Ensure G.name is consistent for all inputs accepted by nodes_or_number.
+ See gh-7396"""
+ G = nx.complete_bipartite_graph(n, m)
+ ans = "Graph named 'complete_bipartite_graph(4, 3)' with 7 nodes and 12 edges"
+ assert str(G) == ans
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_matching.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_matching.py
new file mode 100644
index 00000000..c24659ea
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_matching.py
@@ -0,0 +1,327 @@
+"""Unit tests for the :mod:`networkx.algorithms.bipartite.matching` module."""
+
+import itertools
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.bipartite.matching import (
+ eppstein_matching,
+ hopcroft_karp_matching,
+ maximum_matching,
+ minimum_weight_full_matching,
+ to_vertex_cover,
+)
+
+
+class TestMatching:
+ """Tests for bipartite matching algorithms."""
+
+ def setup_method(self):
+ """Creates a bipartite graph for use in testing matching algorithms.
+
+ The bipartite graph has a maximum cardinality matching that leaves
+ vertex 1 and vertex 10 unmatched. The first six numbers are the left
+ vertices and the next six numbers are the right vertices.
+
+ """
+ self.simple_graph = nx.complete_bipartite_graph(2, 3)
+ self.simple_solution = {0: 2, 1: 3, 2: 0, 3: 1}
+
+ edges = [(0, 7), (0, 8), (2, 6), (2, 9), (3, 8), (4, 8), (4, 9), (5, 11)]
+ self.top_nodes = set(range(6))
+ self.graph = nx.Graph()
+ self.graph.add_nodes_from(range(12))
+ self.graph.add_edges_from(edges)
+
+ # Example bipartite graph from issue 2127
+ G = nx.Graph()
+ G.add_nodes_from(
+ [
+ (1, "C"),
+ (1, "B"),
+ (0, "G"),
+ (1, "F"),
+ (1, "E"),
+ (0, "C"),
+ (1, "D"),
+ (1, "I"),
+ (0, "A"),
+ (0, "D"),
+ (0, "F"),
+ (0, "E"),
+ (0, "H"),
+ (1, "G"),
+ (1, "A"),
+ (0, "I"),
+ (0, "B"),
+ (1, "H"),
+ ]
+ )
+ G.add_edge((1, "C"), (0, "A"))
+ G.add_edge((1, "B"), (0, "A"))
+ G.add_edge((0, "G"), (1, "I"))
+ G.add_edge((0, "G"), (1, "H"))
+ G.add_edge((1, "F"), (0, "A"))
+ G.add_edge((1, "F"), (0, "C"))
+ G.add_edge((1, "F"), (0, "E"))
+ G.add_edge((1, "E"), (0, "A"))
+ G.add_edge((1, "E"), (0, "C"))
+ G.add_edge((0, "C"), (1, "D"))
+ G.add_edge((0, "C"), (1, "I"))
+ G.add_edge((0, "C"), (1, "G"))
+ G.add_edge((0, "C"), (1, "H"))
+ G.add_edge((1, "D"), (0, "A"))
+ G.add_edge((1, "I"), (0, "A"))
+ G.add_edge((1, "I"), (0, "E"))
+ G.add_edge((0, "A"), (1, "G"))
+ G.add_edge((0, "A"), (1, "H"))
+ G.add_edge((0, "E"), (1, "G"))
+ G.add_edge((0, "E"), (1, "H"))
+ self.disconnected_graph = G
+
+ def check_match(self, matching):
+ """Asserts that the matching is what we expect from the bipartite graph
+ constructed in the :meth:`setup` fixture.
+
+ """
+ # For the sake of brevity, rename `matching` to `M`.
+ M = matching
+ matched_vertices = frozenset(itertools.chain(*M.items()))
+ # Assert that the maximum number of vertices (10) is matched.
+ assert matched_vertices == frozenset(range(12)) - {1, 10}
+ # Assert that no vertex appears in two edges, or in other words, that
+ # the matching (u, v) and (v, u) both appear in the matching
+ # dictionary.
+ assert all(u == M[M[u]] for u in range(12) if u in M)
+
+ def check_vertex_cover(self, vertices):
+ """Asserts that the given set of vertices is the vertex cover we
+ expected from the bipartite graph constructed in the :meth:`setup`
+ fixture.
+
+ """
+ # By Konig's theorem, the number of edges in a maximum matching equals
+ # the number of vertices in a minimum vertex cover.
+ assert len(vertices) == 5
+ # Assert that the set is truly a vertex cover.
+ for u, v in self.graph.edges():
+ assert u in vertices or v in vertices
+ # TODO Assert that the vertices are the correct ones.
+
+ def test_eppstein_matching(self):
+ """Tests that David Eppstein's implementation of the Hopcroft--Karp
+ algorithm produces a maximum cardinality matching.
+
+ """
+ self.check_match(eppstein_matching(self.graph, self.top_nodes))
+
+ def test_hopcroft_karp_matching(self):
+ """Tests that the Hopcroft--Karp algorithm produces a maximum
+ cardinality matching in a bipartite graph.
+
+ """
+ self.check_match(hopcroft_karp_matching(self.graph, self.top_nodes))
+
+ def test_to_vertex_cover(self):
+ """Test for converting a maximum matching to a minimum vertex cover."""
+ matching = maximum_matching(self.graph, self.top_nodes)
+ vertex_cover = to_vertex_cover(self.graph, matching, self.top_nodes)
+ self.check_vertex_cover(vertex_cover)
+
+ def test_eppstein_matching_simple(self):
+ match = eppstein_matching(self.simple_graph)
+ assert match == self.simple_solution
+
+ def test_hopcroft_karp_matching_simple(self):
+ match = hopcroft_karp_matching(self.simple_graph)
+ assert match == self.simple_solution
+
+ def test_eppstein_matching_disconnected(self):
+ with pytest.raises(nx.AmbiguousSolution):
+ match = eppstein_matching(self.disconnected_graph)
+
+ def test_hopcroft_karp_matching_disconnected(self):
+ with pytest.raises(nx.AmbiguousSolution):
+ match = hopcroft_karp_matching(self.disconnected_graph)
+
+ def test_issue_2127(self):
+ """Test from issue 2127"""
+ # Build the example DAG
+ G = nx.DiGraph()
+ G.add_edge("A", "C")
+ G.add_edge("A", "B")
+ G.add_edge("C", "E")
+ G.add_edge("C", "D")
+ G.add_edge("E", "G")
+ G.add_edge("E", "F")
+ G.add_edge("G", "I")
+ G.add_edge("G", "H")
+
+ tc = nx.transitive_closure(G)
+ btc = nx.Graph()
+
+ # Create a bipartite graph based on the transitive closure of G
+ for v in tc.nodes():
+ btc.add_node((0, v))
+ btc.add_node((1, v))
+
+ for u, v in tc.edges():
+ btc.add_edge((0, u), (1, v))
+
+ top_nodes = {n for n in btc if n[0] == 0}
+ matching = hopcroft_karp_matching(btc, top_nodes)
+ vertex_cover = to_vertex_cover(btc, matching, top_nodes)
+ independent_set = set(G) - {v for _, v in vertex_cover}
+ assert {"B", "D", "F", "I", "H"} == independent_set
+
+ def test_vertex_cover_issue_2384(self):
+ G = nx.Graph([(0, 3), (1, 3), (1, 4), (2, 3)])
+ matching = maximum_matching(G)
+ vertex_cover = to_vertex_cover(G, matching)
+ for u, v in G.edges():
+ assert u in vertex_cover or v in vertex_cover
+
+ def test_vertex_cover_issue_3306(self):
+ G = nx.Graph()
+ edges = [(0, 2), (1, 0), (1, 1), (1, 2), (2, 2)]
+ G.add_edges_from([((i, "L"), (j, "R")) for i, j in edges])
+
+ matching = maximum_matching(G)
+ vertex_cover = to_vertex_cover(G, matching)
+ for u, v in G.edges():
+ assert u in vertex_cover or v in vertex_cover
+
+ def test_unorderable_nodes(self):
+ a = object()
+ b = object()
+ c = object()
+ d = object()
+ e = object()
+ G = nx.Graph([(a, d), (b, d), (b, e), (c, d)])
+ matching = maximum_matching(G)
+ vertex_cover = to_vertex_cover(G, matching)
+ for u, v in G.edges():
+ assert u in vertex_cover or v in vertex_cover
+
+
+def test_eppstein_matching():
+ """Test in accordance to issue #1927"""
+ G = nx.Graph()
+ G.add_nodes_from(["a", 2, 3, 4], bipartite=0)
+ G.add_nodes_from([1, "b", "c"], bipartite=1)
+ G.add_edges_from([("a", 1), ("a", "b"), (2, "b"), (2, "c"), (3, "c"), (4, 1)])
+ matching = eppstein_matching(G)
+ assert len(matching) == len(maximum_matching(G))
+ assert all(x in set(matching.keys()) for x in set(matching.values()))
+
+
+class TestMinimumWeightFullMatching:
+ @classmethod
+ def setup_class(cls):
+ pytest.importorskip("scipy")
+
+ def test_minimum_weight_full_matching_incomplete_graph(self):
+ B = nx.Graph()
+ B.add_nodes_from([1, 2], bipartite=0)
+ B.add_nodes_from([3, 4], bipartite=1)
+ B.add_edge(1, 4, weight=100)
+ B.add_edge(2, 3, weight=100)
+ B.add_edge(2, 4, weight=50)
+ matching = minimum_weight_full_matching(B)
+ assert matching == {1: 4, 2: 3, 4: 1, 3: 2}
+
+ def test_minimum_weight_full_matching_with_no_full_matching(self):
+ B = nx.Graph()
+ B.add_nodes_from([1, 2, 3], bipartite=0)
+ B.add_nodes_from([4, 5, 6], bipartite=1)
+ B.add_edge(1, 4, weight=100)
+ B.add_edge(2, 4, weight=100)
+ B.add_edge(3, 4, weight=50)
+ B.add_edge(3, 5, weight=50)
+ B.add_edge(3, 6, weight=50)
+ with pytest.raises(ValueError):
+ minimum_weight_full_matching(B)
+
+ def test_minimum_weight_full_matching_square(self):
+ G = nx.complete_bipartite_graph(3, 3)
+ G.add_edge(0, 3, weight=400)
+ G.add_edge(0, 4, weight=150)
+ G.add_edge(0, 5, weight=400)
+ G.add_edge(1, 3, weight=400)
+ G.add_edge(1, 4, weight=450)
+ G.add_edge(1, 5, weight=600)
+ G.add_edge(2, 3, weight=300)
+ G.add_edge(2, 4, weight=225)
+ G.add_edge(2, 5, weight=300)
+ matching = minimum_weight_full_matching(G)
+ assert matching == {0: 4, 1: 3, 2: 5, 4: 0, 3: 1, 5: 2}
+
+ def test_minimum_weight_full_matching_smaller_left(self):
+ G = nx.complete_bipartite_graph(3, 4)
+ G.add_edge(0, 3, weight=400)
+ G.add_edge(0, 4, weight=150)
+ G.add_edge(0, 5, weight=400)
+ G.add_edge(0, 6, weight=1)
+ G.add_edge(1, 3, weight=400)
+ G.add_edge(1, 4, weight=450)
+ G.add_edge(1, 5, weight=600)
+ G.add_edge(1, 6, weight=2)
+ G.add_edge(2, 3, weight=300)
+ G.add_edge(2, 4, weight=225)
+ G.add_edge(2, 5, weight=290)
+ G.add_edge(2, 6, weight=3)
+ matching = minimum_weight_full_matching(G)
+ assert matching == {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1}
+
+ def test_minimum_weight_full_matching_smaller_top_nodes_right(self):
+ G = nx.complete_bipartite_graph(3, 4)
+ G.add_edge(0, 3, weight=400)
+ G.add_edge(0, 4, weight=150)
+ G.add_edge(0, 5, weight=400)
+ G.add_edge(0, 6, weight=1)
+ G.add_edge(1, 3, weight=400)
+ G.add_edge(1, 4, weight=450)
+ G.add_edge(1, 5, weight=600)
+ G.add_edge(1, 6, weight=2)
+ G.add_edge(2, 3, weight=300)
+ G.add_edge(2, 4, weight=225)
+ G.add_edge(2, 5, weight=290)
+ G.add_edge(2, 6, weight=3)
+ matching = minimum_weight_full_matching(G, top_nodes=[3, 4, 5, 6])
+ assert matching == {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1}
+
+ def test_minimum_weight_full_matching_smaller_right(self):
+ G = nx.complete_bipartite_graph(4, 3)
+ G.add_edge(0, 4, weight=400)
+ G.add_edge(0, 5, weight=400)
+ G.add_edge(0, 6, weight=300)
+ G.add_edge(1, 4, weight=150)
+ G.add_edge(1, 5, weight=450)
+ G.add_edge(1, 6, weight=225)
+ G.add_edge(2, 4, weight=400)
+ G.add_edge(2, 5, weight=600)
+ G.add_edge(2, 6, weight=290)
+ G.add_edge(3, 4, weight=1)
+ G.add_edge(3, 5, weight=2)
+ G.add_edge(3, 6, weight=3)
+ matching = minimum_weight_full_matching(G)
+ assert matching == {1: 4, 2: 6, 3: 5, 4: 1, 5: 3, 6: 2}
+
+ def test_minimum_weight_full_matching_negative_weights(self):
+ G = nx.complete_bipartite_graph(2, 2)
+ G.add_edge(0, 2, weight=-2)
+ G.add_edge(0, 3, weight=0.2)
+ G.add_edge(1, 2, weight=-2)
+ G.add_edge(1, 3, weight=0.3)
+ matching = minimum_weight_full_matching(G)
+ assert matching == {0: 3, 1: 2, 2: 1, 3: 0}
+
+ def test_minimum_weight_full_matching_different_weight_key(self):
+ G = nx.complete_bipartite_graph(2, 2)
+ G.add_edge(0, 2, mass=2)
+ G.add_edge(0, 3, mass=0.2)
+ G.add_edge(1, 2, mass=1)
+ G.add_edge(1, 3, mass=2)
+ matching = minimum_weight_full_matching(G, weight="mass")
+ assert matching == {0: 3, 1: 2, 2: 1, 3: 0}
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_matrix.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_matrix.py
new file mode 100644
index 00000000..53d83115
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_matrix.py
@@ -0,0 +1,84 @@
+import pytest
+
+np = pytest.importorskip("numpy")
+sp = pytest.importorskip("scipy")
+sparse = pytest.importorskip("scipy.sparse")
+
+
+import networkx as nx
+from networkx.algorithms import bipartite
+from networkx.utils import edges_equal
+
+
+class TestBiadjacencyMatrix:
+ def test_biadjacency_matrix_weight(self):
+ G = nx.path_graph(5)
+ G.add_edge(0, 1, weight=2, other=4)
+ X = [1, 3]
+ Y = [0, 2, 4]
+ M = bipartite.biadjacency_matrix(G, X, weight="weight")
+ assert M[0, 0] == 2
+ M = bipartite.biadjacency_matrix(G, X, weight="other")
+ assert M[0, 0] == 4
+
+ def test_biadjacency_matrix(self):
+ tops = [2, 5, 10]
+ bots = [5, 10, 15]
+ for i in range(len(tops)):
+ G = bipartite.random_graph(tops[i], bots[i], 0.2)
+ top = [n for n, d in G.nodes(data=True) if d["bipartite"] == 0]
+ M = bipartite.biadjacency_matrix(G, top)
+ assert M.shape[0] == tops[i]
+ assert M.shape[1] == bots[i]
+
+ def test_biadjacency_matrix_order(self):
+ G = nx.path_graph(5)
+ G.add_edge(0, 1, weight=2)
+ X = [3, 1]
+ Y = [4, 2, 0]
+ M = bipartite.biadjacency_matrix(G, X, Y, weight="weight")
+ assert M[1, 2] == 2
+
+ def test_biadjacency_matrix_empty_graph(self):
+ G = nx.empty_graph(2)
+ M = nx.bipartite.biadjacency_matrix(G, [0])
+ assert np.array_equal(M.toarray(), np.array([[0]]))
+
+ def test_null_graph(self):
+ with pytest.raises(nx.NetworkXError):
+ bipartite.biadjacency_matrix(nx.Graph(), [])
+
+ def test_empty_graph(self):
+ with pytest.raises(nx.NetworkXError):
+ bipartite.biadjacency_matrix(nx.Graph([(1, 0)]), [])
+
+ def test_duplicate_row(self):
+ with pytest.raises(nx.NetworkXError):
+ bipartite.biadjacency_matrix(nx.Graph([(1, 0)]), [1, 1])
+
+ def test_duplicate_col(self):
+ with pytest.raises(nx.NetworkXError):
+ bipartite.biadjacency_matrix(nx.Graph([(1, 0)]), [0], [1, 1])
+
+ def test_format_keyword(self):
+ with pytest.raises(nx.NetworkXError):
+ bipartite.biadjacency_matrix(nx.Graph([(1, 0)]), [0], format="foo")
+
+ def test_from_biadjacency_roundtrip(self):
+ B1 = nx.path_graph(5)
+ M = bipartite.biadjacency_matrix(B1, [0, 2, 4])
+ B2 = bipartite.from_biadjacency_matrix(M)
+ assert nx.is_isomorphic(B1, B2)
+
+ def test_from_biadjacency_weight(self):
+ M = sparse.csc_matrix([[1, 2], [0, 3]])
+ B = bipartite.from_biadjacency_matrix(M)
+ assert edges_equal(B.edges(), [(0, 2), (0, 3), (1, 3)])
+ B = bipartite.from_biadjacency_matrix(M, edge_attribute="weight")
+ e = [(0, 2, {"weight": 1}), (0, 3, {"weight": 2}), (1, 3, {"weight": 3})]
+ assert edges_equal(B.edges(data=True), e)
+
+ def test_from_biadjacency_multigraph(self):
+ M = sparse.csc_matrix([[1, 2], [0, 3]])
+ B = bipartite.from_biadjacency_matrix(M, create_using=nx.MultiGraph())
+ assert edges_equal(B.edges(), [(0, 2), (0, 3), (0, 3), (1, 3), (1, 3), (1, 3)])
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_project.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_project.py
new file mode 100644
index 00000000..076bb42b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_project.py
@@ -0,0 +1,407 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms import bipartite
+from networkx.utils import edges_equal, nodes_equal
+
+
+class TestBipartiteProject:
+ def test_path_projected_graph(self):
+ G = nx.path_graph(4)
+ P = bipartite.projected_graph(G, [1, 3])
+ assert nodes_equal(list(P), [1, 3])
+ assert edges_equal(list(P.edges()), [(1, 3)])
+ P = bipartite.projected_graph(G, [0, 2])
+ assert nodes_equal(list(P), [0, 2])
+ assert edges_equal(list(P.edges()), [(0, 2)])
+ G = nx.MultiGraph([(0, 1)])
+ with pytest.raises(nx.NetworkXError, match="not defined for multigraphs"):
+ bipartite.projected_graph(G, [0])
+
+ def test_path_projected_properties_graph(self):
+ G = nx.path_graph(4)
+ G.add_node(1, name="one")
+ G.add_node(2, name="two")
+ P = bipartite.projected_graph(G, [1, 3])
+ assert nodes_equal(list(P), [1, 3])
+ assert edges_equal(list(P.edges()), [(1, 3)])
+ assert P.nodes[1]["name"] == G.nodes[1]["name"]
+ P = bipartite.projected_graph(G, [0, 2])
+ assert nodes_equal(list(P), [0, 2])
+ assert edges_equal(list(P.edges()), [(0, 2)])
+ assert P.nodes[2]["name"] == G.nodes[2]["name"]
+
+ def test_path_collaboration_projected_graph(self):
+ G = nx.path_graph(4)
+ P = bipartite.collaboration_weighted_projected_graph(G, [1, 3])
+ assert nodes_equal(list(P), [1, 3])
+ assert edges_equal(list(P.edges()), [(1, 3)])
+ P[1][3]["weight"] = 1
+ P = bipartite.collaboration_weighted_projected_graph(G, [0, 2])
+ assert nodes_equal(list(P), [0, 2])
+ assert edges_equal(list(P.edges()), [(0, 2)])
+ P[0][2]["weight"] = 1
+
+ def test_directed_path_collaboration_projected_graph(self):
+ G = nx.DiGraph()
+ nx.add_path(G, range(4))
+ P = bipartite.collaboration_weighted_projected_graph(G, [1, 3])
+ assert nodes_equal(list(P), [1, 3])
+ assert edges_equal(list(P.edges()), [(1, 3)])
+ P[1][3]["weight"] = 1
+ P = bipartite.collaboration_weighted_projected_graph(G, [0, 2])
+ assert nodes_equal(list(P), [0, 2])
+ assert edges_equal(list(P.edges()), [(0, 2)])
+ P[0][2]["weight"] = 1
+
+ def test_path_weighted_projected_graph(self):
+ G = nx.path_graph(4)
+
+ with pytest.raises(nx.NetworkXAlgorithmError):
+ bipartite.weighted_projected_graph(G, [1, 2, 3, 3])
+
+ P = bipartite.weighted_projected_graph(G, [1, 3])
+ assert nodes_equal(list(P), [1, 3])
+ assert edges_equal(list(P.edges()), [(1, 3)])
+ P[1][3]["weight"] = 1
+ P = bipartite.weighted_projected_graph(G, [0, 2])
+ assert nodes_equal(list(P), [0, 2])
+ assert edges_equal(list(P.edges()), [(0, 2)])
+ P[0][2]["weight"] = 1
+
+ def test_digraph_weighted_projection(self):
+ G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 4)])
+ P = bipartite.overlap_weighted_projected_graph(G, [1, 3])
+ assert nx.get_edge_attributes(P, "weight") == {(1, 3): 1.0}
+ assert len(P) == 2
+
+ def test_path_weighted_projected_directed_graph(self):
+ G = nx.DiGraph()
+ nx.add_path(G, range(4))
+ P = bipartite.weighted_projected_graph(G, [1, 3])
+ assert nodes_equal(list(P), [1, 3])
+ assert edges_equal(list(P.edges()), [(1, 3)])
+ P[1][3]["weight"] = 1
+ P = bipartite.weighted_projected_graph(G, [0, 2])
+ assert nodes_equal(list(P), [0, 2])
+ assert edges_equal(list(P.edges()), [(0, 2)])
+ P[0][2]["weight"] = 1
+
+ def test_star_projected_graph(self):
+ G = nx.star_graph(3)
+ P = bipartite.projected_graph(G, [1, 2, 3])
+ assert nodes_equal(list(P), [1, 2, 3])
+ assert edges_equal(list(P.edges()), [(1, 2), (1, 3), (2, 3)])
+ P = bipartite.weighted_projected_graph(G, [1, 2, 3])
+ assert nodes_equal(list(P), [1, 2, 3])
+ assert edges_equal(list(P.edges()), [(1, 2), (1, 3), (2, 3)])
+
+ P = bipartite.projected_graph(G, [0])
+ assert nodes_equal(list(P), [0])
+ assert edges_equal(list(P.edges()), [])
+
+ def test_project_multigraph(self):
+ G = nx.Graph()
+ G.add_edge("a", 1)
+ G.add_edge("b", 1)
+ G.add_edge("a", 2)
+ G.add_edge("b", 2)
+ P = bipartite.projected_graph(G, "ab")
+ assert edges_equal(list(P.edges()), [("a", "b")])
+ P = bipartite.weighted_projected_graph(G, "ab")
+ assert edges_equal(list(P.edges()), [("a", "b")])
+ P = bipartite.projected_graph(G, "ab", multigraph=True)
+ assert edges_equal(list(P.edges()), [("a", "b"), ("a", "b")])
+
+ def test_project_collaboration(self):
+ G = nx.Graph()
+ G.add_edge("a", 1)
+ G.add_edge("b", 1)
+ G.add_edge("b", 2)
+ G.add_edge("c", 2)
+ G.add_edge("c", 3)
+ G.add_edge("c", 4)
+ G.add_edge("b", 4)
+ P = bipartite.collaboration_weighted_projected_graph(G, "abc")
+ assert P["a"]["b"]["weight"] == 1
+ assert P["b"]["c"]["weight"] == 2
+
+ def test_directed_projection(self):
+ G = nx.DiGraph()
+ G.add_edge("A", 1)
+ G.add_edge(1, "B")
+ G.add_edge("A", 2)
+ G.add_edge("B", 2)
+ P = bipartite.projected_graph(G, "AB")
+ assert edges_equal(list(P.edges()), [("A", "B")])
+ P = bipartite.weighted_projected_graph(G, "AB")
+ assert edges_equal(list(P.edges()), [("A", "B")])
+ assert P["A"]["B"]["weight"] == 1
+
+ P = bipartite.projected_graph(G, "AB", multigraph=True)
+ assert edges_equal(list(P.edges()), [("A", "B")])
+
+ G = nx.DiGraph()
+ G.add_edge("A", 1)
+ G.add_edge(1, "B")
+ G.add_edge("A", 2)
+ G.add_edge(2, "B")
+ P = bipartite.projected_graph(G, "AB")
+ assert edges_equal(list(P.edges()), [("A", "B")])
+ P = bipartite.weighted_projected_graph(G, "AB")
+ assert edges_equal(list(P.edges()), [("A", "B")])
+ assert P["A"]["B"]["weight"] == 2
+
+ P = bipartite.projected_graph(G, "AB", multigraph=True)
+ assert edges_equal(list(P.edges()), [("A", "B"), ("A", "B")])
+
+
+class TestBipartiteWeightedProjection:
+ @classmethod
+ def setup_class(cls):
+ # Tore Opsahl's example
+ # http://toreopsahl.com/2009/05/01/projecting-two-mode-networks-onto-weighted-one-mode-networks/
+ cls.G = nx.Graph()
+ cls.G.add_edge("A", 1)
+ cls.G.add_edge("A", 2)
+ cls.G.add_edge("B", 1)
+ cls.G.add_edge("B", 2)
+ cls.G.add_edge("B", 3)
+ cls.G.add_edge("B", 4)
+ cls.G.add_edge("B", 5)
+ cls.G.add_edge("C", 1)
+ cls.G.add_edge("D", 3)
+ cls.G.add_edge("E", 4)
+ cls.G.add_edge("E", 5)
+ cls.G.add_edge("E", 6)
+ cls.G.add_edge("F", 6)
+ # Graph based on figure 6 from Newman (2001)
+ cls.N = nx.Graph()
+ cls.N.add_edge("A", 1)
+ cls.N.add_edge("A", 2)
+ cls.N.add_edge("A", 3)
+ cls.N.add_edge("B", 1)
+ cls.N.add_edge("B", 2)
+ cls.N.add_edge("B", 3)
+ cls.N.add_edge("C", 1)
+ cls.N.add_edge("D", 1)
+ cls.N.add_edge("E", 3)
+
+ def test_project_weighted_shared(self):
+ edges = [
+ ("A", "B", 2),
+ ("A", "C", 1),
+ ("B", "C", 1),
+ ("B", "D", 1),
+ ("B", "E", 2),
+ ("E", "F", 1),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.weighted_projected_graph(self.G, "ABCDEF")
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in list(P.edges()):
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ edges = [
+ ("A", "B", 3),
+ ("A", "E", 1),
+ ("A", "C", 1),
+ ("A", "D", 1),
+ ("B", "E", 1),
+ ("B", "C", 1),
+ ("B", "D", 1),
+ ("C", "D", 1),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.weighted_projected_graph(self.N, "ABCDE")
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in list(P.edges()):
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ def test_project_weighted_newman(self):
+ edges = [
+ ("A", "B", 1.5),
+ ("A", "C", 0.5),
+ ("B", "C", 0.5),
+ ("B", "D", 1),
+ ("B", "E", 2),
+ ("E", "F", 1),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.collaboration_weighted_projected_graph(self.G, "ABCDEF")
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in list(P.edges()):
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ edges = [
+ ("A", "B", 11 / 6.0),
+ ("A", "E", 1 / 2.0),
+ ("A", "C", 1 / 3.0),
+ ("A", "D", 1 / 3.0),
+ ("B", "E", 1 / 2.0),
+ ("B", "C", 1 / 3.0),
+ ("B", "D", 1 / 3.0),
+ ("C", "D", 1 / 3.0),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.collaboration_weighted_projected_graph(self.N, "ABCDE")
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in list(P.edges()):
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ def test_project_weighted_ratio(self):
+ edges = [
+ ("A", "B", 2 / 6.0),
+ ("A", "C", 1 / 6.0),
+ ("B", "C", 1 / 6.0),
+ ("B", "D", 1 / 6.0),
+ ("B", "E", 2 / 6.0),
+ ("E", "F", 1 / 6.0),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.weighted_projected_graph(self.G, "ABCDEF", ratio=True)
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in list(P.edges()):
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ edges = [
+ ("A", "B", 3 / 3.0),
+ ("A", "E", 1 / 3.0),
+ ("A", "C", 1 / 3.0),
+ ("A", "D", 1 / 3.0),
+ ("B", "E", 1 / 3.0),
+ ("B", "C", 1 / 3.0),
+ ("B", "D", 1 / 3.0),
+ ("C", "D", 1 / 3.0),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.weighted_projected_graph(self.N, "ABCDE", ratio=True)
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in list(P.edges()):
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ def test_project_weighted_overlap(self):
+ edges = [
+ ("A", "B", 2 / 2.0),
+ ("A", "C", 1 / 1.0),
+ ("B", "C", 1 / 1.0),
+ ("B", "D", 1 / 1.0),
+ ("B", "E", 2 / 3.0),
+ ("E", "F", 1 / 1.0),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.overlap_weighted_projected_graph(self.G, "ABCDEF", jaccard=False)
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in list(P.edges()):
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ edges = [
+ ("A", "B", 3 / 3.0),
+ ("A", "E", 1 / 1.0),
+ ("A", "C", 1 / 1.0),
+ ("A", "D", 1 / 1.0),
+ ("B", "E", 1 / 1.0),
+ ("B", "C", 1 / 1.0),
+ ("B", "D", 1 / 1.0),
+ ("C", "D", 1 / 1.0),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.overlap_weighted_projected_graph(self.N, "ABCDE", jaccard=False)
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in list(P.edges()):
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ def test_project_weighted_jaccard(self):
+ edges = [
+ ("A", "B", 2 / 5.0),
+ ("A", "C", 1 / 2.0),
+ ("B", "C", 1 / 5.0),
+ ("B", "D", 1 / 5.0),
+ ("B", "E", 2 / 6.0),
+ ("E", "F", 1 / 3.0),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.overlap_weighted_projected_graph(self.G, "ABCDEF")
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in list(P.edges()):
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ edges = [
+ ("A", "B", 3 / 3.0),
+ ("A", "E", 1 / 3.0),
+ ("A", "C", 1 / 3.0),
+ ("A", "D", 1 / 3.0),
+ ("B", "E", 1 / 3.0),
+ ("B", "C", 1 / 3.0),
+ ("B", "D", 1 / 3.0),
+ ("C", "D", 1 / 1.0),
+ ]
+ Panswer = nx.Graph()
+ Panswer.add_weighted_edges_from(edges)
+ P = bipartite.overlap_weighted_projected_graph(self.N, "ABCDE")
+ assert edges_equal(list(P.edges()), Panswer.edges())
+ for u, v in P.edges():
+ assert P[u][v]["weight"] == Panswer[u][v]["weight"]
+
+ def test_generic_weighted_projected_graph_simple(self):
+ def shared(G, u, v):
+ return len(set(G[u]) & set(G[v]))
+
+ B = nx.path_graph(5)
+ G = bipartite.generic_weighted_projected_graph(
+ B, [0, 2, 4], weight_function=shared
+ )
+ assert nodes_equal(list(G), [0, 2, 4])
+ assert edges_equal(
+ list(G.edges(data=True)),
+ [(0, 2, {"weight": 1}), (2, 4, {"weight": 1})],
+ )
+
+ G = bipartite.generic_weighted_projected_graph(B, [0, 2, 4])
+ assert nodes_equal(list(G), [0, 2, 4])
+ assert edges_equal(
+ list(G.edges(data=True)),
+ [(0, 2, {"weight": 1}), (2, 4, {"weight": 1})],
+ )
+ B = nx.DiGraph()
+ nx.add_path(B, range(5))
+ G = bipartite.generic_weighted_projected_graph(B, [0, 2, 4])
+ assert nodes_equal(list(G), [0, 2, 4])
+ assert edges_equal(
+ list(G.edges(data=True)), [(0, 2, {"weight": 1}), (2, 4, {"weight": 1})]
+ )
+
+ def test_generic_weighted_projected_graph_custom(self):
+ def jaccard(G, u, v):
+ unbrs = set(G[u])
+ vnbrs = set(G[v])
+ return len(unbrs & vnbrs) / len(unbrs | vnbrs)
+
+ def my_weight(G, u, v, weight="weight"):
+ w = 0
+ for nbr in set(G[u]) & set(G[v]):
+ w += G.edges[u, nbr].get(weight, 1) + G.edges[v, nbr].get(weight, 1)
+ return w
+
+ B = nx.bipartite.complete_bipartite_graph(2, 2)
+ for i, (u, v) in enumerate(B.edges()):
+ B.edges[u, v]["weight"] = i + 1
+ G = bipartite.generic_weighted_projected_graph(
+ B, [0, 1], weight_function=jaccard
+ )
+ assert edges_equal(list(G.edges(data=True)), [(0, 1, {"weight": 1.0})])
+ G = bipartite.generic_weighted_projected_graph(
+ B, [0, 1], weight_function=my_weight
+ )
+ assert edges_equal(list(G.edges(data=True)), [(0, 1, {"weight": 10})])
+ G = bipartite.generic_weighted_projected_graph(B, [0, 1])
+ assert edges_equal(list(G.edges(data=True)), [(0, 1, {"weight": 2})])
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_redundancy.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_redundancy.py
new file mode 100644
index 00000000..8d979db8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_redundancy.py
@@ -0,0 +1,35 @@
+"""Unit tests for the :mod:`networkx.algorithms.bipartite.redundancy` module."""
+
+import pytest
+
+from networkx import NetworkXError, cycle_graph
+from networkx.algorithms.bipartite import complete_bipartite_graph, node_redundancy
+
+
+def test_no_redundant_nodes():
+ G = complete_bipartite_graph(2, 2)
+
+ # when nodes is None
+ rc = node_redundancy(G)
+ assert all(redundancy == 1 for redundancy in rc.values())
+
+ # when set of nodes is specified
+ rc = node_redundancy(G, (2, 3))
+ assert rc == {2: 1.0, 3: 1.0}
+
+
+def test_redundant_nodes():
+ G = cycle_graph(6)
+ edge = {0, 3}
+ G.add_edge(*edge)
+ redundancy = node_redundancy(G)
+ for v in edge:
+ assert redundancy[v] == 2 / 3
+ for v in set(G) - edge:
+ assert redundancy[v] == 1
+
+
+def test_not_enough_neighbors():
+ with pytest.raises(NetworkXError):
+ G = complete_bipartite_graph(1, 2)
+ node_redundancy(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_spectral_bipartivity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_spectral_bipartivity.py
new file mode 100644
index 00000000..b9406497
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/bipartite/tests/test_spectral_bipartivity.py
@@ -0,0 +1,80 @@
+import pytest
+
+pytest.importorskip("scipy")
+
+import networkx as nx
+from networkx.algorithms.bipartite import spectral_bipartivity as sb
+
+# Examples from Figure 1
+# E. Estrada and J. A. Rodríguez-Velázquez, "Spectral measures of
+# bipartivity in complex networks", PhysRev E 72, 046105 (2005)
+
+
+class TestSpectralBipartivity:
+ def test_star_like(self):
+ # star-like
+
+ G = nx.star_graph(2)
+ G.add_edge(1, 2)
+ assert sb(G) == pytest.approx(0.843, abs=1e-3)
+
+ G = nx.star_graph(3)
+ G.add_edge(1, 2)
+ assert sb(G) == pytest.approx(0.871, abs=1e-3)
+
+ G = nx.star_graph(4)
+ G.add_edge(1, 2)
+ assert sb(G) == pytest.approx(0.890, abs=1e-3)
+
+ def test_k23_like(self):
+ # K2,3-like
+ G = nx.complete_bipartite_graph(2, 3)
+ G.add_edge(0, 1)
+ assert sb(G) == pytest.approx(0.769, abs=1e-3)
+
+ G = nx.complete_bipartite_graph(2, 3)
+ G.add_edge(2, 4)
+ assert sb(G) == pytest.approx(0.829, abs=1e-3)
+
+ G = nx.complete_bipartite_graph(2, 3)
+ G.add_edge(2, 4)
+ G.add_edge(3, 4)
+ assert sb(G) == pytest.approx(0.731, abs=1e-3)
+
+ G = nx.complete_bipartite_graph(2, 3)
+ G.add_edge(0, 1)
+ G.add_edge(2, 4)
+ assert sb(G) == pytest.approx(0.692, abs=1e-3)
+
+ G = nx.complete_bipartite_graph(2, 3)
+ G.add_edge(2, 4)
+ G.add_edge(3, 4)
+ G.add_edge(0, 1)
+ assert sb(G) == pytest.approx(0.645, abs=1e-3)
+
+ G = nx.complete_bipartite_graph(2, 3)
+ G.add_edge(2, 4)
+ G.add_edge(3, 4)
+ G.add_edge(2, 3)
+ assert sb(G) == pytest.approx(0.645, abs=1e-3)
+
+ G = nx.complete_bipartite_graph(2, 3)
+ G.add_edge(2, 4)
+ G.add_edge(3, 4)
+ G.add_edge(2, 3)
+ G.add_edge(0, 1)
+ assert sb(G) == pytest.approx(0.597, abs=1e-3)
+
+ def test_single_nodes(self):
+ # single nodes
+ G = nx.complete_bipartite_graph(2, 3)
+ G.add_edge(2, 4)
+ sbn = sb(G, nodes=[1, 2])
+ assert sbn[1] == pytest.approx(0.85, abs=1e-2)
+ assert sbn[2] == pytest.approx(0.77, abs=1e-2)
+
+ G = nx.complete_bipartite_graph(2, 3)
+ G.add_edge(0, 1)
+ sbn = sb(G, nodes=[1, 2])
+ assert sbn[1] == pytest.approx(0.73, abs=1e-2)
+ assert sbn[2] == pytest.approx(0.82, abs=1e-2)