aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/generators
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/generators
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/generators')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/__init__.py34
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/atlas.dat.gzbin0 -> 8887 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/atlas.py180
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/classic.py1068
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/cographs.py68
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/community.py1070
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/degree_seq.py867
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/directed.py501
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/duplication.py174
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/ego.py66
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/expanders.py474
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/geometric.py1048
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/harary_graph.py199
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/internet_as_graphs.py441
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/intersection.py125
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/interval_graph.py70
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/joint_degree_seq.py664
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/lattice.py367
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/line.py500
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/mycielski.py110
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/nonisomorphic_trees.py212
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/random_clustered.py117
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/random_graphs.py1400
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/small.py993
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/social.py554
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/spectral_graph_forge.py120
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/stochastic.py54
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/sudoku.py131
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_atlas.py75
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_classic.py640
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_cographs.py18
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_community.py362
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_degree_seq.py230
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_directed.py163
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_duplication.py103
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_ego.py39
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_expanders.py162
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_geometric.py488
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_harary_graph.py133
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_internet_as_graphs.py176
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_intersection.py28
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_interval_graph.py144
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_joint_degree_seq.py125
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_lattice.py246
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_line.py309
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_mycielski.py30
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_nonisomorphic_trees.py68
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_random_clustered.py33
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_random_graphs.py478
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_small.py208
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_spectral_graph_forge.py49
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_stochastic.py72
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_sudoku.py92
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_time_series.py64
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_trees.py195
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/tests/test_triads.py15
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/time_series.py74
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/trees.py1071
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/generators/triads.py94
60 files changed, 17591 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/__init__.py b/.venv/lib/python3.12/site-packages/networkx/generators/__init__.py
new file mode 100644
index 00000000..6ec027c2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/__init__.py
@@ -0,0 +1,34 @@
+"""
+A package for generating various graphs in networkx.
+
+"""
+
+from networkx.generators.atlas import *
+from networkx.generators.classic import *
+from networkx.generators.cographs import *
+from networkx.generators.community import *
+from networkx.generators.degree_seq import *
+from networkx.generators.directed import *
+from networkx.generators.duplication import *
+from networkx.generators.ego import *
+from networkx.generators.expanders import *
+from networkx.generators.geometric import *
+from networkx.generators.harary_graph import *
+from networkx.generators.internet_as_graphs import *
+from networkx.generators.intersection import *
+from networkx.generators.interval_graph import *
+from networkx.generators.joint_degree_seq import *
+from networkx.generators.lattice import *
+from networkx.generators.line import *
+from networkx.generators.mycielski import *
+from networkx.generators.nonisomorphic_trees import *
+from networkx.generators.random_clustered import *
+from networkx.generators.random_graphs import *
+from networkx.generators.small import *
+from networkx.generators.social import *
+from networkx.generators.spectral_graph_forge import *
+from networkx.generators.stochastic import *
+from networkx.generators.sudoku import *
+from networkx.generators.time_series import *
+from networkx.generators.trees import *
+from networkx.generators.triads import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/atlas.dat.gz b/.venv/lib/python3.12/site-packages/networkx/generators/atlas.dat.gz
new file mode 100644
index 00000000..b0a98701
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/atlas.dat.gz
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/atlas.py b/.venv/lib/python3.12/site-packages/networkx/generators/atlas.py
new file mode 100644
index 00000000..c5dd8d2d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/atlas.py
@@ -0,0 +1,180 @@
+"""
+Generators for the small graph atlas.
+"""
+
+import gzip
+import importlib.resources
+import os
+import os.path
+from itertools import islice
+
+import networkx as nx
+
+__all__ = ["graph_atlas", "graph_atlas_g"]
+
+#: The total number of graphs in the atlas.
+#:
+#: The graphs are labeled starting from 0 and extending to (but not
+#: including) this number.
+NUM_GRAPHS = 1253
+
+#: The path to the data file containing the graph edge lists.
+#:
+#: This is the absolute path of the gzipped text file containing the
+#: edge list for each graph in the atlas. The file contains one entry
+#: per graph in the atlas, in sequential order, starting from graph
+#: number 0 and extending through graph number 1252 (see
+#: :data:`NUM_GRAPHS`). Each entry looks like
+#:
+#: .. sourcecode:: text
+#:
+#: GRAPH 6
+#: NODES 3
+#: 0 1
+#: 0 2
+#:
+#: where the first two lines are the graph's index in the atlas and the
+#: number of nodes in the graph, and the remaining lines are the edge
+#: list.
+#:
+#: This file was generated from a Python list of graphs via code like
+#: the following::
+#:
+#: import gzip
+#: from networkx.generators.atlas import graph_atlas_g
+#: from networkx.readwrite.edgelist import write_edgelist
+#:
+#: with gzip.open('atlas.dat.gz', 'wb') as f:
+#: for i, G in enumerate(graph_atlas_g()):
+#: f.write(bytes(f'GRAPH {i}\n', encoding='utf-8'))
+#: f.write(bytes(f'NODES {len(G)}\n', encoding='utf-8'))
+#: write_edgelist(G, f, data=False)
+#:
+
+# Path to the atlas file
+ATLAS_FILE = importlib.resources.files("networkx.generators") / "atlas.dat.gz"
+
+
+def _generate_graphs():
+ """Sequentially read the file containing the edge list data for the
+ graphs in the atlas and generate the graphs one at a time.
+
+ This function reads the file given in :data:`.ATLAS_FILE`.
+
+ """
+ with gzip.open(ATLAS_FILE, "rb") as f:
+ line = f.readline()
+ while line and line.startswith(b"GRAPH"):
+ # The first two lines of each entry tell us the index of the
+ # graph in the list and the number of nodes in the graph.
+ # They look like this:
+ #
+ # GRAPH 3
+ # NODES 2
+ #
+ graph_index = int(line[6:].rstrip())
+ line = f.readline()
+ num_nodes = int(line[6:].rstrip())
+ # The remaining lines contain the edge list, until the next
+ # GRAPH line (or until the end of the file).
+ edgelist = []
+ line = f.readline()
+ while line and not line.startswith(b"GRAPH"):
+ edgelist.append(line.rstrip())
+ line = f.readline()
+ G = nx.Graph()
+ G.name = f"G{graph_index}"
+ G.add_nodes_from(range(num_nodes))
+ G.add_edges_from(tuple(map(int, e.split())) for e in edgelist)
+ yield G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def graph_atlas(i):
+ """Returns graph number `i` from the Graph Atlas.
+
+ For more information, see :func:`.graph_atlas_g`.
+
+ Parameters
+ ----------
+ i : int
+ The index of the graph from the atlas to get. The graph at index
+ 0 is assumed to be the null graph.
+
+ Returns
+ -------
+ list
+ A list of :class:`~networkx.Graph` objects, the one at index *i*
+ corresponding to the graph *i* in the Graph Atlas.
+
+ See also
+ --------
+ graph_atlas_g
+
+ Notes
+ -----
+ The time required by this function increases linearly with the
+ argument `i`, since it reads a large file sequentially in order to
+ generate the graph [1]_.
+
+ References
+ ----------
+ .. [1] Ronald C. Read and Robin J. Wilson, *An Atlas of Graphs*.
+ Oxford University Press, 1998.
+
+ """
+ if not (0 <= i < NUM_GRAPHS):
+ raise ValueError(f"index must be between 0 and {NUM_GRAPHS}")
+ return next(islice(_generate_graphs(), i, None))
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def graph_atlas_g():
+ """Returns the list of all graphs with up to seven nodes named in the
+ Graph Atlas.
+
+ The graphs are listed in increasing order by
+
+ 1. number of nodes,
+ 2. number of edges,
+ 3. degree sequence (for example 111223 < 112222),
+ 4. number of automorphisms,
+
+ in that order, with three exceptions as described in the *Notes*
+ section below. This causes the list to correspond with the index of
+ the graphs in the Graph Atlas [atlas]_, with the first graph,
+ ``G[0]``, being the null graph.
+
+ Returns
+ -------
+ list
+ A list of :class:`~networkx.Graph` objects, the one at index *i*
+ corresponding to the graph *i* in the Graph Atlas.
+
+ See also
+ --------
+ graph_atlas
+
+ Notes
+ -----
+ This function may be expensive in both time and space, since it
+ reads a large file sequentially in order to populate the list.
+
+ Although the NetworkX atlas functions match the order of graphs
+ given in the "Atlas of Graphs" book, there are (at least) three
+ errors in the ordering described in the book. The following three
+ pairs of nodes violate the lexicographically nondecreasing sorted
+ degree sequence rule:
+
+ - graphs 55 and 56 with degree sequences 001111 and 000112,
+ - graphs 1007 and 1008 with degree sequences 3333444 and 3333336,
+ - graphs 1012 and 1213 with degree sequences 1244555 and 1244456.
+
+ References
+ ----------
+ .. [atlas] Ronald C. Read and Robin J. Wilson,
+ *An Atlas of Graphs*.
+ Oxford University Press, 1998.
+
+ """
+ return list(_generate_graphs())
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/classic.py b/.venv/lib/python3.12/site-packages/networkx/generators/classic.py
new file mode 100644
index 00000000..a461e7bd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/classic.py
@@ -0,0 +1,1068 @@
+"""Generators for some classic graphs.
+
+The typical graph builder function is called as follows:
+
+>>> G = nx.complete_graph(100)
+
+returning the complete graph on n nodes labeled 0, .., 99
+as a simple graph. Except for `empty_graph`, all the functions
+in this module return a Graph class (i.e. a simple, undirected graph).
+
+"""
+
+import itertools
+import numbers
+
+import networkx as nx
+from networkx.classes import Graph
+from networkx.exception import NetworkXError
+from networkx.utils import nodes_or_number, pairwise
+
+__all__ = [
+ "balanced_tree",
+ "barbell_graph",
+ "binomial_tree",
+ "complete_graph",
+ "complete_multipartite_graph",
+ "circular_ladder_graph",
+ "circulant_graph",
+ "cycle_graph",
+ "dorogovtsev_goltsev_mendes_graph",
+ "empty_graph",
+ "full_rary_tree",
+ "kneser_graph",
+ "ladder_graph",
+ "lollipop_graph",
+ "null_graph",
+ "path_graph",
+ "star_graph",
+ "tadpole_graph",
+ "trivial_graph",
+ "turan_graph",
+ "wheel_graph",
+]
+
+
+# -------------------------------------------------------------------
+# Some Classic Graphs
+# -------------------------------------------------------------------
+
+
+def _tree_edges(n, r):
+ if n == 0:
+ return
+ # helper function for trees
+ # yields edges in rooted tree at 0 with n nodes and branching ratio r
+ nodes = iter(range(n))
+ parents = [next(nodes)] # stack of max length r
+ while parents:
+ source = parents.pop(0)
+ for i in range(r):
+ try:
+ target = next(nodes)
+ parents.append(target)
+ yield source, target
+ except StopIteration:
+ break
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def full_rary_tree(r, n, create_using=None):
+ """Creates a full r-ary tree of `n` nodes.
+
+ Sometimes called a k-ary, n-ary, or m-ary tree.
+ "... all non-leaf nodes have exactly r children and all levels
+ are full except for some rightmost position of the bottom level
+ (if a leaf at the bottom level is missing, then so are all of the
+ leaves to its right." [1]_
+
+ .. plot::
+
+ >>> nx.draw(nx.full_rary_tree(2, 10))
+
+ Parameters
+ ----------
+ r : int
+ branching factor of the tree
+ n : int
+ Number of nodes in the tree
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ An r-ary tree with n nodes
+
+ References
+ ----------
+ .. [1] An introduction to data structures and algorithms,
+ James Andrew Storer, Birkhauser Boston 2001, (page 225).
+ """
+ G = empty_graph(n, create_using)
+ G.add_edges_from(_tree_edges(n, r))
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def kneser_graph(n, k):
+ """Returns the Kneser Graph with parameters `n` and `k`.
+
+ The Kneser Graph has nodes that are k-tuples (subsets) of the integers
+ between 0 and ``n-1``. Nodes are adjacent if their corresponding sets are disjoint.
+
+ Parameters
+ ----------
+ n: int
+ Number of integers from which to make node subsets.
+ Subsets are drawn from ``set(range(n))``.
+ k: int
+ Size of the subsets.
+
+ Returns
+ -------
+ G : NetworkX Graph
+
+ Examples
+ --------
+ >>> G = nx.kneser_graph(5, 2)
+ >>> G.number_of_nodes()
+ 10
+ >>> G.number_of_edges()
+ 15
+ >>> nx.is_isomorphic(G, nx.petersen_graph())
+ True
+ """
+ if n <= 0:
+ raise NetworkXError("n should be greater than zero")
+ if k <= 0 or k > n:
+ raise NetworkXError("k should be greater than zero and smaller than n")
+
+ G = nx.Graph()
+ # Create all k-subsets of [0, 1, ..., n-1]
+ subsets = list(itertools.combinations(range(n), k))
+
+ if 2 * k > n:
+ G.add_nodes_from(subsets)
+
+ universe = set(range(n))
+ comb = itertools.combinations # only to make it all fit on one line
+ G.add_edges_from((s, t) for s in subsets for t in comb(universe - set(s), k))
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def balanced_tree(r, h, create_using=None):
+ """Returns the perfectly balanced `r`-ary tree of height `h`.
+
+ .. plot::
+
+ >>> nx.draw(nx.balanced_tree(2, 3))
+
+ Parameters
+ ----------
+ r : int
+ Branching factor of the tree; each node will have `r`
+ children.
+
+ h : int
+ Height of the tree.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : NetworkX graph
+ A balanced `r`-ary tree of height `h`.
+
+ Notes
+ -----
+ This is the rooted tree where all leaves are at distance `h` from
+ the root. The root has degree `r` and all other internal nodes
+ have degree `r + 1`.
+
+ Node labels are integers, starting from zero.
+
+ A balanced tree is also known as a *complete r-ary tree*.
+
+ """
+ # The number of nodes in the balanced tree is `1 + r + ... + r^h`,
+ # which is computed by using the closed-form formula for a geometric
+ # sum with ratio `r`. In the special case that `r` is 1, the number
+ # of nodes is simply `h + 1` (since the tree is actually a path
+ # graph).
+ if r == 1:
+ n = h + 1
+ else:
+ # This must be an integer if both `r` and `h` are integers. If
+ # they are not, we force integer division anyway.
+ n = (1 - r ** (h + 1)) // (1 - r)
+ return full_rary_tree(r, n, create_using=create_using)
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def barbell_graph(m1, m2, create_using=None):
+ """Returns the Barbell Graph: two complete graphs connected by a path.
+
+ .. plot::
+
+ >>> nx.draw(nx.barbell_graph(4, 2))
+
+ Parameters
+ ----------
+ m1 : int
+ Size of the left and right barbells, must be greater than 2.
+
+ m2 : int
+ Length of the path connecting the barbells.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Only undirected Graphs are supported.
+
+ Returns
+ -------
+ G : NetworkX graph
+ A barbell graph.
+
+ Notes
+ -----
+
+
+ Two identical complete graphs $K_{m1}$ form the left and right bells,
+ and are connected by a path $P_{m2}$.
+
+ The `2*m1+m2` nodes are numbered
+ `0, ..., m1-1` for the left barbell,
+ `m1, ..., m1+m2-1` for the path,
+ and `m1+m2, ..., 2*m1+m2-1` for the right barbell.
+
+ The 3 subgraphs are joined via the edges `(m1-1, m1)` and
+ `(m1+m2-1, m1+m2)`. If `m2=0`, this is merely two complete
+ graphs joined together.
+
+ This graph is an extremal example in David Aldous
+ and Jim Fill's e-text on Random Walks on Graphs.
+
+ """
+ if m1 < 2:
+ raise NetworkXError("Invalid graph description, m1 should be >=2")
+ if m2 < 0:
+ raise NetworkXError("Invalid graph description, m2 should be >=0")
+
+ # left barbell
+ G = complete_graph(m1, create_using)
+ if G.is_directed():
+ raise NetworkXError("Directed Graph not supported")
+
+ # connecting path
+ G.add_nodes_from(range(m1, m1 + m2 - 1))
+ if m2 > 1:
+ G.add_edges_from(pairwise(range(m1, m1 + m2)))
+
+ # right barbell
+ G.add_edges_from(
+ (u, v) for u in range(m1 + m2, 2 * m1 + m2) for v in range(u + 1, 2 * m1 + m2)
+ )
+
+ # connect it up
+ G.add_edge(m1 - 1, m1)
+ if m2 > 0:
+ G.add_edge(m1 + m2 - 1, m1 + m2)
+
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def binomial_tree(n, create_using=None):
+ """Returns the Binomial Tree of order n.
+
+ The binomial tree of order 0 consists of a single node. A binomial tree of order k
+ is defined recursively by linking two binomial trees of order k-1: the root of one is
+ the leftmost child of the root of the other.
+
+ .. plot::
+
+ >>> nx.draw(nx.binomial_tree(3))
+
+ Parameters
+ ----------
+ n : int
+ Order of the binomial tree.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : NetworkX graph
+ A binomial tree of $2^n$ nodes and $2^n - 1$ edges.
+
+ """
+ G = nx.empty_graph(1, create_using)
+
+ N = 1
+ for i in range(n):
+ # Use G.edges() to ensure 2-tuples. G.edges is 3-tuple for MultiGraph
+ edges = [(u + N, v + N) for (u, v) in G.edges()]
+ G.add_edges_from(edges)
+ G.add_edge(0, N)
+ N *= 2
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number(0)
+def complete_graph(n, create_using=None):
+ """Return the complete graph `K_n` with n nodes.
+
+ A complete graph on `n` nodes means that all pairs
+ of distinct nodes have an edge connecting them.
+
+ .. plot::
+
+ >>> nx.draw(nx.complete_graph(5))
+
+ Parameters
+ ----------
+ n : int or iterable container of nodes
+ If n is an integer, nodes are from range(n).
+ If n is a container of nodes, those nodes appear in the graph.
+ Warning: n is not checked for duplicates and if present the
+ resulting graph may not be as desired. Make sure you have no duplicates.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Examples
+ --------
+ >>> G = nx.complete_graph(9)
+ >>> len(G)
+ 9
+ >>> G.size()
+ 36
+ >>> G = nx.complete_graph(range(11, 14))
+ >>> list(G.nodes())
+ [11, 12, 13]
+ >>> G = nx.complete_graph(4, nx.DiGraph())
+ >>> G.is_directed()
+ True
+
+ """
+ _, nodes = n
+ G = empty_graph(nodes, create_using)
+ if len(nodes) > 1:
+ if G.is_directed():
+ edges = itertools.permutations(nodes, 2)
+ else:
+ edges = itertools.combinations(nodes, 2)
+ G.add_edges_from(edges)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def circular_ladder_graph(n, create_using=None):
+ """Returns the circular ladder graph $CL_n$ of length n.
+
+ $CL_n$ consists of two concentric n-cycles in which
+ each of the n pairs of concentric nodes are joined by an edge.
+
+ Node labels are the integers 0 to n-1
+
+ .. plot::
+
+ >>> nx.draw(nx.circular_ladder_graph(5))
+
+ """
+ G = ladder_graph(n, create_using)
+ G.add_edge(0, n - 1)
+ G.add_edge(n, 2 * n - 1)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def circulant_graph(n, offsets, create_using=None):
+ r"""Returns the circulant graph $Ci_n(x_1, x_2, ..., x_m)$ with $n$ nodes.
+
+ The circulant graph $Ci_n(x_1, ..., x_m)$ consists of $n$ nodes $0, ..., n-1$
+ such that node $i$ is connected to nodes $(i + x) \mod n$ and $(i - x) \mod n$
+ for all $x$ in $x_1, ..., x_m$. Thus $Ci_n(1)$ is a cycle graph.
+
+ .. plot::
+
+ >>> nx.draw(nx.circulant_graph(10, [1]))
+
+ Parameters
+ ----------
+ n : integer
+ The number of nodes in the graph.
+ offsets : list of integers
+ A list of node offsets, $x_1$ up to $x_m$, as described above.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ NetworkX Graph of type create_using
+
+ Examples
+ --------
+ Many well-known graph families are subfamilies of the circulant graphs;
+ for example, to create the cycle graph on n points, we connect every
+ node to nodes on either side (with offset plus or minus one). For n = 10,
+
+ >>> G = nx.circulant_graph(10, [1])
+ >>> edges = [
+ ... (0, 9),
+ ... (0, 1),
+ ... (1, 2),
+ ... (2, 3),
+ ... (3, 4),
+ ... (4, 5),
+ ... (5, 6),
+ ... (6, 7),
+ ... (7, 8),
+ ... (8, 9),
+ ... ]
+ >>> sorted(edges) == sorted(G.edges())
+ True
+
+ Similarly, we can create the complete graph
+ on 5 points with the set of offsets [1, 2]:
+
+ >>> G = nx.circulant_graph(5, [1, 2])
+ >>> edges = [
+ ... (0, 1),
+ ... (0, 2),
+ ... (0, 3),
+ ... (0, 4),
+ ... (1, 2),
+ ... (1, 3),
+ ... (1, 4),
+ ... (2, 3),
+ ... (2, 4),
+ ... (3, 4),
+ ... ]
+ >>> sorted(edges) == sorted(G.edges())
+ True
+
+ """
+ G = empty_graph(n, create_using)
+ for i in range(n):
+ for j in offsets:
+ G.add_edge(i, (i - j) % n)
+ G.add_edge(i, (i + j) % n)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number(0)
+def cycle_graph(n, create_using=None):
+ """Returns the cycle graph $C_n$ of cyclically connected nodes.
+
+ $C_n$ is a path with its two end-nodes connected.
+
+ .. plot::
+
+ >>> nx.draw(nx.cycle_graph(5))
+
+ Parameters
+ ----------
+ n : int or iterable container of nodes
+ If n is an integer, nodes are from `range(n)`.
+ If n is a container of nodes, those nodes appear in the graph.
+ Warning: n is not checked for duplicates and if present the
+ resulting graph may not be as desired. Make sure you have no duplicates.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Notes
+ -----
+ If create_using is directed, the direction is in increasing order.
+
+ """
+ _, nodes = n
+ G = empty_graph(nodes, create_using)
+ G.add_edges_from(pairwise(nodes, cyclic=True))
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def dorogovtsev_goltsev_mendes_graph(n, create_using=None):
+ """Returns the hierarchically constructed Dorogovtsev--Goltsev--Mendes graph.
+
+ The Dorogovtsev--Goltsev--Mendes [1]_ procedure deterministically produces a
+ scale-free graph with ``3/2 * (3**(n-1) + 1)`` nodes
+ and ``3**n`` edges for a given `n`.
+
+ Note that `n` denotes the number of times the state transition is applied,
+ starting from the base graph with ``n = 0`` (no transitions), as in [2]_.
+ This is different from the parameter ``t = n - 1`` in [1]_.
+
+ .. plot::
+
+ >>> nx.draw(nx.dorogovtsev_goltsev_mendes_graph(3))
+
+ Parameters
+ ----------
+ n : integer
+ The generation number.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. Directed graphs and multigraphs are not supported.
+
+ Returns
+ -------
+ G : NetworkX `Graph`
+
+ Raises
+ ------
+ NetworkXError
+ If `n` is less than zero.
+
+ If `create_using` is a directed graph or multigraph.
+
+ Examples
+ --------
+ >>> G = nx.dorogovtsev_goltsev_mendes_graph(3)
+ >>> G.number_of_nodes()
+ 15
+ >>> G.number_of_edges()
+ 27
+ >>> nx.is_planar(G)
+ True
+
+ References
+ ----------
+ .. [1] S. N. Dorogovtsev, A. V. Goltsev and J. F. F. Mendes,
+ "Pseudofractal scale-free web", Physical Review E 65, 066122, 2002.
+ https://arxiv.org/pdf/cond-mat/0112143.pdf
+ .. [2] Weisstein, Eric W. "Dorogovtsev--Goltsev--Mendes Graph".
+ From MathWorld--A Wolfram Web Resource.
+ https://mathworld.wolfram.com/Dorogovtsev-Goltsev-MendesGraph.html
+ """
+ if n < 0:
+ raise NetworkXError("n must be greater than or equal to 0")
+
+ G = empty_graph(0, create_using)
+ if G.is_directed():
+ raise NetworkXError("directed graph not supported")
+ if G.is_multigraph():
+ raise NetworkXError("multigraph not supported")
+
+ G.add_edge(0, 1)
+ new_node = 2 # next node to be added
+ for _ in range(n): # iterate over number of generations.
+ new_edges = []
+ for u, v in G.edges():
+ new_edges.append((u, new_node))
+ new_edges.append((v, new_node))
+ new_node += 1
+
+ G.add_edges_from(new_edges)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number(0)
+def empty_graph(n=0, create_using=None, default=Graph):
+ """Returns the empty graph with n nodes and zero edges.
+
+ .. plot::
+
+ >>> nx.draw(nx.empty_graph(5))
+
+ Parameters
+ ----------
+ n : int or iterable container of nodes (default = 0)
+ If n is an integer, nodes are from `range(n)`.
+ If n is a container of nodes, those nodes appear in the graph.
+ create_using : Graph Instance, Constructor or None
+ Indicator of type of graph to return.
+ If a Graph-type instance, then clear and use it.
+ If None, use the `default` constructor.
+ If a constructor, call it to create an empty graph.
+ default : Graph constructor (optional, default = nx.Graph)
+ The constructor to use if create_using is None.
+ If None, then nx.Graph is used.
+ This is used when passing an unknown `create_using` value
+ through your home-grown function to `empty_graph` and
+ you want a default constructor other than nx.Graph.
+
+ Examples
+ --------
+ >>> G = nx.empty_graph(10)
+ >>> G.number_of_nodes()
+ 10
+ >>> G.number_of_edges()
+ 0
+ >>> G = nx.empty_graph("ABC")
+ >>> G.number_of_nodes()
+ 3
+ >>> sorted(G)
+ ['A', 'B', 'C']
+
+ Notes
+ -----
+ The variable create_using should be a Graph Constructor or a
+ "graph"-like object. Constructors, e.g. `nx.Graph` or `nx.MultiGraph`
+ will be used to create the returned graph. "graph"-like objects
+ will be cleared (nodes and edges will be removed) and refitted as
+ an empty "graph" with nodes specified in n. This capability
+ is useful for specifying the class-nature of the resulting empty
+ "graph" (i.e. Graph, DiGraph, MyWeirdGraphClass, etc.).
+
+ The variable create_using has three main uses:
+ Firstly, the variable create_using can be used to create an
+ empty digraph, multigraph, etc. For example,
+
+ >>> n = 10
+ >>> G = nx.empty_graph(n, create_using=nx.DiGraph)
+
+ will create an empty digraph on n nodes.
+
+ Secondly, one can pass an existing graph (digraph, multigraph,
+ etc.) via create_using. For example, if G is an existing graph
+ (resp. digraph, multigraph, etc.), then empty_graph(n, create_using=G)
+ will empty G (i.e. delete all nodes and edges using G.clear())
+ and then add n nodes and zero edges, and return the modified graph.
+
+ Thirdly, when constructing your home-grown graph creation function
+ you can use empty_graph to construct the graph by passing a user
+ defined create_using to empty_graph. In this case, if you want the
+ default constructor to be other than nx.Graph, specify `default`.
+
+ >>> def mygraph(n, create_using=None):
+ ... G = nx.empty_graph(n, create_using, nx.MultiGraph)
+ ... G.add_edges_from([(0, 1), (0, 1)])
+ ... return G
+ >>> G = mygraph(3)
+ >>> G.is_multigraph()
+ True
+ >>> G = mygraph(3, nx.Graph)
+ >>> G.is_multigraph()
+ False
+
+ See also create_empty_copy(G).
+
+ """
+ if create_using is None:
+ G = default()
+ elif isinstance(create_using, type):
+ G = create_using()
+ elif not hasattr(create_using, "adj"):
+ raise TypeError("create_using is not a valid NetworkX graph type or instance")
+ else:
+ # create_using is a NetworkX style Graph
+ create_using.clear()
+ G = create_using
+
+ _, nodes = n
+ G.add_nodes_from(nodes)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def ladder_graph(n, create_using=None):
+ """Returns the Ladder graph of length n.
+
+ This is two paths of n nodes, with
+ each pair connected by a single edge.
+
+ Node labels are the integers 0 to 2*n - 1.
+
+ .. plot::
+
+ >>> nx.draw(nx.ladder_graph(5))
+
+ """
+ G = empty_graph(2 * n, create_using)
+ if G.is_directed():
+ raise NetworkXError("Directed Graph not supported")
+ G.add_edges_from(pairwise(range(n)))
+ G.add_edges_from(pairwise(range(n, 2 * n)))
+ G.add_edges_from((v, v + n) for v in range(n))
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number([0, 1])
+def lollipop_graph(m, n, create_using=None):
+ """Returns the Lollipop Graph; ``K_m`` connected to ``P_n``.
+
+ This is the Barbell Graph without the right barbell.
+
+ .. plot::
+
+ >>> nx.draw(nx.lollipop_graph(3, 4))
+
+ Parameters
+ ----------
+ m, n : int or iterable container of nodes
+ If an integer, nodes are from ``range(m)`` and ``range(m, m+n)``.
+ If a container of nodes, those nodes appear in the graph.
+ Warning: `m` and `n` are not checked for duplicates and if present the
+ resulting graph may not be as desired. Make sure you have no duplicates.
+
+ The nodes for `m` appear in the complete graph $K_m$ and the nodes
+ for `n` appear in the path $P_n$
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ Networkx graph
+ A complete graph with `m` nodes connected to a path of length `n`.
+
+ Notes
+ -----
+ The 2 subgraphs are joined via an edge ``(m-1, m)``.
+ If ``n=0``, this is merely a complete graph.
+
+ (This graph is an extremal example in David Aldous and Jim
+ Fill's etext on Random Walks on Graphs.)
+
+ """
+ m, m_nodes = m
+ M = len(m_nodes)
+ if M < 2:
+ raise NetworkXError("Invalid description: m should indicate at least 2 nodes")
+
+ n, n_nodes = n
+ if isinstance(m, numbers.Integral) and isinstance(n, numbers.Integral):
+ n_nodes = list(range(M, M + n))
+ N = len(n_nodes)
+
+ # the ball
+ G = complete_graph(m_nodes, create_using)
+ if G.is_directed():
+ raise NetworkXError("Directed Graph not supported")
+
+ # the stick
+ G.add_nodes_from(n_nodes)
+ if N > 1:
+ G.add_edges_from(pairwise(n_nodes))
+
+ if len(G) != M + N:
+ raise NetworkXError("Nodes must be distinct in containers m and n")
+
+ # connect ball to stick
+ if M > 0 and N > 0:
+ G.add_edge(m_nodes[-1], n_nodes[0])
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def null_graph(create_using=None):
+ """Returns the Null graph with no nodes or edges.
+
+ See empty_graph for the use of create_using.
+
+ """
+ G = empty_graph(0, create_using)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number(0)
+def path_graph(n, create_using=None):
+ """Returns the Path graph `P_n` of linearly connected nodes.
+
+ .. plot::
+
+ >>> nx.draw(nx.path_graph(5))
+
+ Parameters
+ ----------
+ n : int or iterable
+ If an integer, nodes are 0 to n - 1.
+ If an iterable of nodes, in the order they appear in the path.
+ Warning: n is not checked for duplicates and if present the
+ resulting graph may not be as desired. Make sure you have no duplicates.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ """
+ _, nodes = n
+ G = empty_graph(nodes, create_using)
+ G.add_edges_from(pairwise(nodes))
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number(0)
+def star_graph(n, create_using=None):
+ """Return the star graph
+
+ The star graph consists of one center node connected to n outer nodes.
+
+ .. plot::
+
+ >>> nx.draw(nx.star_graph(6))
+
+ Parameters
+ ----------
+ n : int or iterable
+ If an integer, node labels are 0 to n with center 0.
+ If an iterable of nodes, the center is the first.
+ Warning: n is not checked for duplicates and if present the
+ resulting graph may not be as desired. Make sure you have no duplicates.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Notes
+ -----
+ The graph has n+1 nodes for integer n.
+ So star_graph(3) is the same as star_graph(range(4)).
+ """
+ n, nodes = n
+ if isinstance(n, numbers.Integral):
+ nodes.append(int(n)) # there should be n+1 nodes
+ G = empty_graph(nodes, create_using)
+ if G.is_directed():
+ raise NetworkXError("Directed Graph not supported")
+
+ if len(nodes) > 1:
+ hub, *spokes = nodes
+ G.add_edges_from((hub, node) for node in spokes)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number([0, 1])
+def tadpole_graph(m, n, create_using=None):
+ """Returns the (m,n)-tadpole graph; ``C_m`` connected to ``P_n``.
+
+ This graph on m+n nodes connects a cycle of size `m` to a path of length `n`.
+ It looks like a tadpole. It is also called a kite graph or a dragon graph.
+
+ .. plot::
+
+ >>> nx.draw(nx.tadpole_graph(3, 5))
+
+ Parameters
+ ----------
+ m, n : int or iterable container of nodes
+ If an integer, nodes are from ``range(m)`` and ``range(m,m+n)``.
+ If a container of nodes, those nodes appear in the graph.
+ Warning: `m` and `n` are not checked for duplicates and if present the
+ resulting graph may not be as desired.
+
+ The nodes for `m` appear in the cycle graph $C_m$ and the nodes
+ for `n` appear in the path $P_n$.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ Networkx graph
+ A cycle of size `m` connected to a path of length `n`.
+
+ Raises
+ ------
+ NetworkXError
+ If ``m < 2``. The tadpole graph is undefined for ``m<2``.
+
+ Notes
+ -----
+ The 2 subgraphs are joined via an edge ``(m-1, m)``.
+ If ``n=0``, this is a cycle graph.
+ `m` and/or `n` can be a container of nodes instead of an integer.
+
+ """
+ m, m_nodes = m
+ M = len(m_nodes)
+ if M < 2:
+ raise NetworkXError("Invalid description: m should indicate at least 2 nodes")
+
+ n, n_nodes = n
+ if isinstance(m, numbers.Integral) and isinstance(n, numbers.Integral):
+ n_nodes = list(range(M, M + n))
+
+ # the circle
+ G = cycle_graph(m_nodes, create_using)
+ if G.is_directed():
+ raise NetworkXError("Directed Graph not supported")
+
+ # the stick
+ nx.add_path(G, [m_nodes[-1]] + list(n_nodes))
+
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def trivial_graph(create_using=None):
+ """Return the Trivial graph with one node (with label 0) and no edges.
+
+ .. plot::
+
+ >>> nx.draw(nx.trivial_graph(), with_labels=True)
+
+ """
+ G = empty_graph(1, create_using)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def turan_graph(n, r):
+ r"""Return the Turan Graph
+
+ The Turan Graph is a complete multipartite graph on $n$ nodes
+ with $r$ disjoint subsets. That is, edges connect each node to
+ every node not in its subset.
+
+ Given $n$ and $r$, we create a complete multipartite graph with
+ $r-(n \mod r)$ partitions of size $n/r$, rounded down, and
+ $n \mod r$ partitions of size $n/r+1$, rounded down.
+
+ .. plot::
+
+ >>> nx.draw(nx.turan_graph(6, 2))
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ r : int
+ The number of partitions.
+ Must be less than or equal to n.
+
+ Notes
+ -----
+ Must satisfy $1 <= r <= n$.
+ The graph has $(r-1)(n^2)/(2r)$ edges, rounded down.
+ """
+
+ if not 1 <= r <= n:
+ raise NetworkXError("Must satisfy 1 <= r <= n")
+
+ partitions = [n // r] * (r - (n % r)) + [n // r + 1] * (n % r)
+ G = complete_multipartite_graph(*partitions)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number(0)
+def wheel_graph(n, create_using=None):
+ """Return the wheel graph
+
+ The wheel graph consists of a hub node connected to a cycle of (n-1) nodes.
+
+ .. plot::
+
+ >>> nx.draw(nx.wheel_graph(5))
+
+ Parameters
+ ----------
+ n : int or iterable
+ If an integer, node labels are 0 to n with center 0.
+ If an iterable of nodes, the center is the first.
+ Warning: n is not checked for duplicates and if present the
+ resulting graph may not be as desired. Make sure you have no duplicates.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Node labels are the integers 0 to n - 1.
+ """
+ _, nodes = n
+ G = empty_graph(nodes, create_using)
+ if G.is_directed():
+ raise NetworkXError("Directed Graph not supported")
+
+ if len(nodes) > 1:
+ hub, *rim = nodes
+ G.add_edges_from((hub, node) for node in rim)
+ if len(rim) > 1:
+ G.add_edges_from(pairwise(rim, cyclic=True))
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def complete_multipartite_graph(*subset_sizes):
+ """Returns the complete multipartite graph with the specified subset sizes.
+
+ .. plot::
+
+ >>> nx.draw(nx.complete_multipartite_graph(1, 2, 3))
+
+ Parameters
+ ----------
+ subset_sizes : tuple of integers or tuple of node iterables
+ The arguments can either all be integer number of nodes or they
+ can all be iterables of nodes. If integers, they represent the
+ number of nodes in each subset of the multipartite graph.
+ If iterables, each is used to create the nodes for that subset.
+ The length of subset_sizes is the number of subsets.
+
+ Returns
+ -------
+ G : NetworkX Graph
+ Returns the complete multipartite graph with the specified subsets.
+
+ For each node, the node attribute 'subset' is an integer
+ indicating which subset contains the node.
+
+ Examples
+ --------
+ Creating a complete tripartite graph, with subsets of one, two, and three
+ nodes, respectively.
+
+ >>> G = nx.complete_multipartite_graph(1, 2, 3)
+ >>> [G.nodes[u]["subset"] for u in G]
+ [0, 1, 1, 2, 2, 2]
+ >>> list(G.edges(0))
+ [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5)]
+ >>> list(G.edges(2))
+ [(2, 0), (2, 3), (2, 4), (2, 5)]
+ >>> list(G.edges(4))
+ [(4, 0), (4, 1), (4, 2)]
+
+ >>> G = nx.complete_multipartite_graph("a", "bc", "def")
+ >>> [G.nodes[u]["subset"] for u in sorted(G)]
+ [0, 1, 1, 2, 2, 2]
+
+ Notes
+ -----
+ This function generalizes several other graph builder functions.
+
+ - If no subset sizes are given, this returns the null graph.
+ - If a single subset size `n` is given, this returns the empty graph on
+ `n` nodes.
+ - If two subset sizes `m` and `n` are given, this returns the complete
+ bipartite graph on `m + n` nodes.
+ - If subset sizes `1` and `n` are given, this returns the star graph on
+ `n + 1` nodes.
+
+ See also
+ --------
+ complete_bipartite_graph
+ """
+ # The complete multipartite graph is an undirected simple graph.
+ G = Graph()
+
+ if len(subset_sizes) == 0:
+ return G
+
+ # set up subsets of nodes
+ try:
+ extents = pairwise(itertools.accumulate((0,) + subset_sizes))
+ subsets = [range(start, end) for start, end in extents]
+ except TypeError:
+ subsets = subset_sizes
+ else:
+ if any(size < 0 for size in subset_sizes):
+ raise NetworkXError(f"Negative number of nodes not valid: {subset_sizes}")
+
+ # add nodes with subset attribute
+ # while checking that ints are not mixed with iterables
+ try:
+ for i, subset in enumerate(subsets):
+ G.add_nodes_from(subset, subset=i)
+ except TypeError as err:
+ raise NetworkXError("Arguments must be all ints or all iterables") from err
+
+ # Across subsets, all nodes should be adjacent.
+ # We can use itertools.combinations() because undirected.
+ for subset1, subset2 in itertools.combinations(subsets, 2):
+ G.add_edges_from(itertools.product(subset1, subset2))
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/cographs.py b/.venv/lib/python3.12/site-packages/networkx/generators/cographs.py
new file mode 100644
index 00000000..6635b32f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/cographs.py
@@ -0,0 +1,68 @@
+r"""Generators for cographs
+
+A cograph is a graph containing no path on four vertices.
+Cographs or $P_4$-free graphs can be obtained from a single vertex
+by disjoint union and complementation operations.
+
+References
+----------
+.. [0] D.G. Corneil, H. Lerchs, L.Stewart Burlingham,
+ "Complement reducible graphs",
+ Discrete Applied Mathematics, Volume 3, Issue 3, 1981, Pages 163-174,
+ ISSN 0166-218X.
+"""
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+__all__ = ["random_cograph"]
+
+
+@py_random_state(1)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_cograph(n, seed=None):
+ r"""Returns a random cograph with $2 ^ n$ nodes.
+
+ A cograph is a graph containing no path on four vertices.
+ Cographs or $P_4$-free graphs can be obtained from a single vertex
+ by disjoint union and complementation operations.
+
+ This generator starts off from a single vertex and performs disjoint
+ union and full join operations on itself.
+ The decision on which operation will take place is random.
+
+ Parameters
+ ----------
+ n : int
+ The order of the cograph.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : A random graph containing no path on four vertices.
+
+ See Also
+ --------
+ full_join
+ union
+
+ References
+ ----------
+ .. [1] D.G. Corneil, H. Lerchs, L.Stewart Burlingham,
+ "Complement reducible graphs",
+ Discrete Applied Mathematics, Volume 3, Issue 3, 1981, Pages 163-174,
+ ISSN 0166-218X.
+ """
+ R = nx.empty_graph(1)
+
+ for i in range(n):
+ RR = nx.relabel_nodes(R.copy(), lambda x: x + len(R))
+
+ if seed.randint(0, 1) == 0:
+ R = nx.full_join(R, RR)
+ else:
+ R = nx.disjoint_union(R, RR)
+
+ return R
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/community.py b/.venv/lib/python3.12/site-packages/networkx/generators/community.py
new file mode 100644
index 00000000..a7f2294c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/community.py
@@ -0,0 +1,1070 @@
+"""Generators for classes of graphs used in studying social networks."""
+
+import itertools
+import math
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+__all__ = [
+ "caveman_graph",
+ "connected_caveman_graph",
+ "relaxed_caveman_graph",
+ "random_partition_graph",
+ "planted_partition_graph",
+ "gaussian_random_partition_graph",
+ "ring_of_cliques",
+ "windmill_graph",
+ "stochastic_block_model",
+ "LFR_benchmark_graph",
+]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def caveman_graph(l, k):
+ """Returns a caveman graph of `l` cliques of size `k`.
+
+ Parameters
+ ----------
+ l : int
+ Number of cliques
+ k : int
+ Size of cliques
+
+ Returns
+ -------
+ G : NetworkX Graph
+ caveman graph
+
+ Notes
+ -----
+ This returns an undirected graph, it can be converted to a directed
+ graph using :func:`nx.to_directed`, or a multigraph using
+ ``nx.MultiGraph(nx.caveman_graph(l, k))``. Only the undirected version is
+ described in [1]_ and it is unclear which of the directed
+ generalizations is most useful.
+
+ Examples
+ --------
+ >>> G = nx.caveman_graph(3, 3)
+
+ See also
+ --------
+
+ connected_caveman_graph
+
+ References
+ ----------
+ .. [1] Watts, D. J. 'Networks, Dynamics, and the Small-World Phenomenon.'
+ Amer. J. Soc. 105, 493-527, 1999.
+ """
+ # l disjoint cliques of size k
+ G = nx.empty_graph(l * k)
+ if k > 1:
+ for start in range(0, l * k, k):
+ edges = itertools.combinations(range(start, start + k), 2)
+ G.add_edges_from(edges)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def connected_caveman_graph(l, k):
+ """Returns a connected caveman graph of `l` cliques of size `k`.
+
+ The connected caveman graph is formed by creating `n` cliques of size
+ `k`, then a single edge in each clique is rewired to a node in an
+ adjacent clique.
+
+ Parameters
+ ----------
+ l : int
+ number of cliques
+ k : int
+ size of cliques (k at least 2 or NetworkXError is raised)
+
+ Returns
+ -------
+ G : NetworkX Graph
+ connected caveman graph
+
+ Raises
+ ------
+ NetworkXError
+ If the size of cliques `k` is smaller than 2.
+
+ Notes
+ -----
+ This returns an undirected graph, it can be converted to a directed
+ graph using :func:`nx.to_directed`, or a multigraph using
+ ``nx.MultiGraph(nx.caveman_graph(l, k))``. Only the undirected version is
+ described in [1]_ and it is unclear which of the directed
+ generalizations is most useful.
+
+ Examples
+ --------
+ >>> G = nx.connected_caveman_graph(3, 3)
+
+ References
+ ----------
+ .. [1] Watts, D. J. 'Networks, Dynamics, and the Small-World Phenomenon.'
+ Amer. J. Soc. 105, 493-527, 1999.
+ """
+ if k < 2:
+ raise nx.NetworkXError(
+ "The size of cliques in a connected caveman graph must be at least 2."
+ )
+
+ G = nx.caveman_graph(l, k)
+ for start in range(0, l * k, k):
+ G.remove_edge(start, start + 1)
+ G.add_edge(start, (start - 1) % (l * k))
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def relaxed_caveman_graph(l, k, p, seed=None):
+ """Returns a relaxed caveman graph.
+
+ A relaxed caveman graph starts with `l` cliques of size `k`. Edges are
+ then randomly rewired with probability `p` to link different cliques.
+
+ Parameters
+ ----------
+ l : int
+ Number of groups
+ k : int
+ Size of cliques
+ p : float
+ Probability of rewiring each edge.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : NetworkX Graph
+ Relaxed Caveman Graph
+
+ Raises
+ ------
+ NetworkXError
+ If p is not in [0,1]
+
+ Examples
+ --------
+ >>> G = nx.relaxed_caveman_graph(2, 3, 0.1, seed=42)
+
+ References
+ ----------
+ .. [1] Santo Fortunato, Community Detection in Graphs,
+ Physics Reports Volume 486, Issues 3-5, February 2010, Pages 75-174.
+ https://arxiv.org/abs/0906.0612
+ """
+ G = nx.caveman_graph(l, k)
+ nodes = list(G)
+ for u, v in G.edges():
+ if seed.random() < p: # rewire the edge
+ x = seed.choice(nodes)
+ if G.has_edge(u, x):
+ continue
+ G.remove_edge(u, v)
+ G.add_edge(u, x)
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_partition_graph(sizes, p_in, p_out, seed=None, directed=False):
+ """Returns the random partition graph with a partition of sizes.
+
+ A partition graph is a graph of communities with sizes defined by
+ s in sizes. Nodes in the same group are connected with probability
+ p_in and nodes of different groups are connected with probability
+ p_out.
+
+ Parameters
+ ----------
+ sizes : list of ints
+ Sizes of groups
+ p_in : float
+ probability of edges with in groups
+ p_out : float
+ probability of edges between groups
+ directed : boolean optional, default=False
+ Whether to create a directed graph
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : NetworkX Graph or DiGraph
+ random partition graph of size sum(gs)
+
+ Raises
+ ------
+ NetworkXError
+ If p_in or p_out is not in [0,1]
+
+ Examples
+ --------
+ >>> G = nx.random_partition_graph([10, 10, 10], 0.25, 0.01)
+ >>> len(G)
+ 30
+ >>> partition = G.graph["partition"]
+ >>> len(partition)
+ 3
+
+ Notes
+ -----
+ This is a generalization of the planted-l-partition described in
+ [1]_. It allows for the creation of groups of any size.
+
+ The partition is store as a graph attribute 'partition'.
+
+ References
+ ----------
+ .. [1] Santo Fortunato 'Community Detection in Graphs' Physical Reports
+ Volume 486, Issue 3-5 p. 75-174. https://arxiv.org/abs/0906.0612
+ """
+ # Use geometric method for O(n+m) complexity algorithm
+ # partition = nx.community_sets(nx.get_node_attributes(G, 'affiliation'))
+ if not 0.0 <= p_in <= 1.0:
+ raise nx.NetworkXError("p_in must be in [0,1]")
+ if not 0.0 <= p_out <= 1.0:
+ raise nx.NetworkXError("p_out must be in [0,1]")
+
+ # create connection matrix
+ num_blocks = len(sizes)
+ p = [[p_out for s in range(num_blocks)] for r in range(num_blocks)]
+ for r in range(num_blocks):
+ p[r][r] = p_in
+
+ return stochastic_block_model(
+ sizes,
+ p,
+ nodelist=None,
+ seed=seed,
+ directed=directed,
+ selfloops=False,
+ sparse=True,
+ )
+
+
+@py_random_state(4)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def planted_partition_graph(l, k, p_in, p_out, seed=None, directed=False):
+ """Returns the planted l-partition graph.
+
+ This model partitions a graph with n=l*k vertices in
+ l groups with k vertices each. Vertices of the same
+ group are linked with a probability p_in, and vertices
+ of different groups are linked with probability p_out.
+
+ Parameters
+ ----------
+ l : int
+ Number of groups
+ k : int
+ Number of vertices in each group
+ p_in : float
+ probability of connecting vertices within a group
+ p_out : float
+ probability of connected vertices between groups
+ 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
+
+ Returns
+ -------
+ G : NetworkX Graph or DiGraph
+ planted l-partition graph
+
+ Raises
+ ------
+ NetworkXError
+ If `p_in`, `p_out` are not in `[0, 1]`
+
+ Examples
+ --------
+ >>> G = nx.planted_partition_graph(4, 3, 0.5, 0.1, seed=42)
+
+ See Also
+ --------
+ random_partition_model
+
+ References
+ ----------
+ .. [1] A. Condon, R.M. Karp, Algorithms for graph partitioning
+ on the planted partition model,
+ Random Struct. Algor. 18 (2001) 116-140.
+
+ .. [2] Santo Fortunato 'Community Detection in Graphs' Physical Reports
+ Volume 486, Issue 3-5 p. 75-174. https://arxiv.org/abs/0906.0612
+ """
+ return random_partition_graph([k] * l, p_in, p_out, seed=seed, directed=directed)
+
+
+@py_random_state(6)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def gaussian_random_partition_graph(n, s, v, p_in, p_out, directed=False, seed=None):
+ """Generate a Gaussian random partition graph.
+
+ A Gaussian random partition graph is created by creating k partitions
+ each with a size drawn from a normal distribution with mean s and variance
+ s/v. Nodes are connected within clusters with probability p_in and
+ between clusters with probability p_out[1]
+
+ Parameters
+ ----------
+ n : int
+ Number of nodes in the graph
+ s : float
+ Mean cluster size
+ v : float
+ Shape parameter. The variance of cluster size distribution is s/v.
+ p_in : float
+ Probability of intra cluster connection.
+ p_out : float
+ Probability of inter cluster connection.
+ directed : boolean, optional default=False
+ Whether to create a directed graph or not
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : NetworkX Graph or DiGraph
+ gaussian random partition graph
+
+ Raises
+ ------
+ NetworkXError
+ If s is > n
+ If p_in or p_out is not in [0,1]
+
+ Notes
+ -----
+ Note the number of partitions is dependent on s,v and n, and that the
+ last partition may be considerably smaller, as it is sized to simply
+ fill out the nodes [1]
+
+ See Also
+ --------
+ random_partition_graph
+
+ Examples
+ --------
+ >>> G = nx.gaussian_random_partition_graph(100, 10, 10, 0.25, 0.1)
+ >>> len(G)
+ 100
+
+ References
+ ----------
+ .. [1] Ulrik Brandes, Marco Gaertler, Dorothea Wagner,
+ Experiments on Graph Clustering Algorithms,
+ In the proceedings of the 11th Europ. Symp. Algorithms, 2003.
+ """
+ if s > n:
+ raise nx.NetworkXError("s must be <= n")
+ assigned = 0
+ sizes = []
+ while True:
+ size = int(seed.gauss(s, s / v + 0.5))
+ if size < 1: # how to handle 0 or negative sizes?
+ continue
+ if assigned + size >= n:
+ sizes.append(n - assigned)
+ break
+ assigned += size
+ sizes.append(size)
+ return random_partition_graph(sizes, p_in, p_out, seed=seed, directed=directed)
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def ring_of_cliques(num_cliques, clique_size):
+ """Defines a "ring of cliques" graph.
+
+ A ring of cliques graph is consisting of cliques, connected through single
+ links. Each clique is a complete graph.
+
+ Parameters
+ ----------
+ num_cliques : int
+ Number of cliques
+ clique_size : int
+ Size of cliques
+
+ Returns
+ -------
+ G : NetworkX Graph
+ ring of cliques graph
+
+ Raises
+ ------
+ NetworkXError
+ If the number of cliques is lower than 2 or
+ if the size of cliques is smaller than 2.
+
+ Examples
+ --------
+ >>> G = nx.ring_of_cliques(8, 4)
+
+ See Also
+ --------
+ connected_caveman_graph
+
+ Notes
+ -----
+ The `connected_caveman_graph` graph removes a link from each clique to
+ connect it with the next clique. Instead, the `ring_of_cliques` graph
+ simply adds the link without removing any link from the cliques.
+ """
+ if num_cliques < 2:
+ raise nx.NetworkXError("A ring of cliques must have at least two cliques")
+ if clique_size < 2:
+ raise nx.NetworkXError("The cliques must have at least two nodes")
+
+ G = nx.Graph()
+ for i in range(num_cliques):
+ edges = itertools.combinations(
+ range(i * clique_size, i * clique_size + clique_size), 2
+ )
+ G.add_edges_from(edges)
+ G.add_edge(
+ i * clique_size + 1, (i + 1) * clique_size % (num_cliques * clique_size)
+ )
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def windmill_graph(n, k):
+ """Generate a windmill graph.
+ A windmill graph is a graph of `n` cliques each of size `k` that are all
+ joined at one node.
+ It can be thought of as taking a disjoint union of `n` cliques of size `k`,
+ selecting one point from each, and contracting all of the selected points.
+ Alternatively, one could generate `n` cliques of size `k-1` and one node
+ that is connected to all other nodes in the graph.
+
+ Parameters
+ ----------
+ n : int
+ Number of cliques
+ k : int
+ Size of cliques
+
+ Returns
+ -------
+ G : NetworkX Graph
+ windmill graph with n cliques of size k
+
+ Raises
+ ------
+ NetworkXError
+ If the number of cliques is less than two
+ If the size of the cliques are less than two
+
+ Examples
+ --------
+ >>> G = nx.windmill_graph(4, 5)
+
+ Notes
+ -----
+ The node labeled `0` will be the node connected to all other nodes.
+ Note that windmill graphs are usually denoted `Wd(k,n)`, so the parameters
+ are in the opposite order as the parameters of this method.
+ """
+ if n < 2:
+ msg = "A windmill graph must have at least two cliques"
+ raise nx.NetworkXError(msg)
+ if k < 2:
+ raise nx.NetworkXError("The cliques must have at least two nodes")
+
+ G = nx.disjoint_union_all(
+ itertools.chain(
+ [nx.complete_graph(k)], (nx.complete_graph(k - 1) for _ in range(n - 1))
+ )
+ )
+ G.add_edges_from((0, i) for i in range(k, G.number_of_nodes()))
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def stochastic_block_model(
+ sizes, p, nodelist=None, seed=None, directed=False, selfloops=False, sparse=True
+):
+ """Returns a stochastic block model graph.
+
+ This model partitions the nodes in blocks of arbitrary sizes, and places
+ edges between pairs of nodes independently, with a probability that depends
+ on the blocks.
+
+ Parameters
+ ----------
+ sizes : list of ints
+ Sizes of blocks
+ p : list of list of floats
+ Element (r,s) gives the density of edges going from the nodes
+ of group r to nodes of group s.
+ p must match the number of groups (len(sizes) == len(p)),
+ and it must be symmetric if the graph is undirected.
+ nodelist : list, optional
+ The block tags are assigned according to the node identifiers
+ in nodelist. If nodelist is None, then the ordering is the
+ range [0,sum(sizes)-1].
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ directed : boolean optional, default=False
+ Whether to create a directed graph or not.
+ selfloops : boolean optional, default=False
+ Whether to include self-loops or not.
+ sparse: boolean optional, default=True
+ Use the sparse heuristic to speed up the generator.
+
+ Returns
+ -------
+ g : NetworkX Graph or DiGraph
+ Stochastic block model graph of size sum(sizes)
+
+ Raises
+ ------
+ NetworkXError
+ If probabilities are not in [0,1].
+ If the probability matrix is not square (directed case).
+ If the probability matrix is not symmetric (undirected case).
+ If the sizes list does not match nodelist or the probability matrix.
+ If nodelist contains duplicate.
+
+ Examples
+ --------
+ >>> sizes = [75, 75, 300]
+ >>> probs = [[0.25, 0.05, 0.02], [0.05, 0.35, 0.07], [0.02, 0.07, 0.40]]
+ >>> g = nx.stochastic_block_model(sizes, probs, seed=0)
+ >>> len(g)
+ 450
+ >>> H = nx.quotient_graph(g, g.graph["partition"], relabel=True)
+ >>> for v in H.nodes(data=True):
+ ... print(round(v[1]["density"], 3))
+ 0.245
+ 0.348
+ 0.405
+ >>> for v in H.edges(data=True):
+ ... print(round(1.0 * v[2]["weight"] / (sizes[v[0]] * sizes[v[1]]), 3))
+ 0.051
+ 0.022
+ 0.07
+
+ See Also
+ --------
+ random_partition_graph
+ planted_partition_graph
+ gaussian_random_partition_graph
+ gnp_random_graph
+
+ References
+ ----------
+ .. [1] Holland, P. W., Laskey, K. B., & Leinhardt, S.,
+ "Stochastic blockmodels: First steps",
+ Social networks, 5(2), 109-137, 1983.
+ """
+ # Check if dimensions match
+ if len(sizes) != len(p):
+ raise nx.NetworkXException("'sizes' and 'p' do not match.")
+ # Check for probability symmetry (undirected) and shape (directed)
+ for row in p:
+ if len(p) != len(row):
+ raise nx.NetworkXException("'p' must be a square matrix.")
+ if not directed:
+ p_transpose = [list(i) for i in zip(*p)]
+ for i in zip(p, p_transpose):
+ for j in zip(i[0], i[1]):
+ if abs(j[0] - j[1]) > 1e-08:
+ raise nx.NetworkXException("'p' must be symmetric.")
+ # Check for probability range
+ for row in p:
+ for prob in row:
+ if prob < 0 or prob > 1:
+ raise nx.NetworkXException("Entries of 'p' not in [0,1].")
+ # Check for nodelist consistency
+ if nodelist is not None:
+ if len(nodelist) != sum(sizes):
+ raise nx.NetworkXException("'nodelist' and 'sizes' do not match.")
+ if len(nodelist) != len(set(nodelist)):
+ raise nx.NetworkXException("nodelist contains duplicate.")
+ else:
+ nodelist = range(sum(sizes))
+
+ # Setup the graph conditionally to the directed switch.
+ block_range = range(len(sizes))
+ if directed:
+ g = nx.DiGraph()
+ block_iter = itertools.product(block_range, block_range)
+ else:
+ g = nx.Graph()
+ block_iter = itertools.combinations_with_replacement(block_range, 2)
+ # Split nodelist in a partition (list of sets).
+ size_cumsum = [sum(sizes[0:x]) for x in range(len(sizes) + 1)]
+ g.graph["partition"] = [
+ set(nodelist[size_cumsum[x] : size_cumsum[x + 1]])
+ for x in range(len(size_cumsum) - 1)
+ ]
+ # Setup nodes and graph name
+ for block_id, nodes in enumerate(g.graph["partition"]):
+ for node in nodes:
+ g.add_node(node, block=block_id)
+
+ g.name = "stochastic_block_model"
+
+ # Test for edge existence
+ parts = g.graph["partition"]
+ for i, j in block_iter:
+ if i == j:
+ if directed:
+ if selfloops:
+ edges = itertools.product(parts[i], parts[i])
+ else:
+ edges = itertools.permutations(parts[i], 2)
+ else:
+ edges = itertools.combinations(parts[i], 2)
+ if selfloops:
+ edges = itertools.chain(edges, zip(parts[i], parts[i]))
+ for e in edges:
+ if seed.random() < p[i][j]:
+ g.add_edge(*e)
+ else:
+ edges = itertools.product(parts[i], parts[j])
+ if sparse:
+ if p[i][j] == 1: # Test edges cases p_ij = 0 or 1
+ for e in edges:
+ g.add_edge(*e)
+ elif p[i][j] > 0:
+ while True:
+ try:
+ logrand = math.log(seed.random())
+ skip = math.floor(logrand / math.log(1 - p[i][j]))
+ # consume "skip" edges
+ next(itertools.islice(edges, skip, skip), None)
+ e = next(edges)
+ g.add_edge(*e) # __safe
+ except StopIteration:
+ break
+ else:
+ for e in edges:
+ if seed.random() < p[i][j]:
+ g.add_edge(*e) # __safe
+ return g
+
+
+def _zipf_rv_below(gamma, xmin, threshold, seed):
+ """Returns a random value chosen from the bounded Zipf distribution.
+
+ Repeatedly draws values from the Zipf distribution until the
+ threshold is met, then returns that value.
+ """
+ result = nx.utils.zipf_rv(gamma, xmin, seed)
+ while result > threshold:
+ result = nx.utils.zipf_rv(gamma, xmin, seed)
+ return result
+
+
+def _powerlaw_sequence(gamma, low, high, condition, length, max_iters, seed):
+ """Returns a list of numbers obeying a constrained power law distribution.
+
+ ``gamma`` and ``low`` are the parameters for the Zipf distribution.
+
+ ``high`` is the maximum allowed value for values draw from the Zipf
+ distribution. For more information, see :func:`_zipf_rv_below`.
+
+ ``condition`` and ``length`` are Boolean-valued functions on
+ lists. While generating the list, random values are drawn and
+ appended to the list until ``length`` is satisfied by the created
+ list. Once ``condition`` is satisfied, the sequence generated in
+ this way is returned.
+
+ ``max_iters`` indicates the number of times to generate a list
+ satisfying ``length``. If the number of iterations exceeds this
+ value, :exc:`~networkx.exception.ExceededMaxIterations` is raised.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ """
+ for i in range(max_iters):
+ seq = []
+ while not length(seq):
+ seq.append(_zipf_rv_below(gamma, low, high, seed))
+ if condition(seq):
+ return seq
+ raise nx.ExceededMaxIterations("Could not create power law sequence")
+
+
+def _hurwitz_zeta(x, q, tolerance):
+ """The Hurwitz zeta function, or the Riemann zeta function of two arguments.
+
+ ``x`` must be greater than one and ``q`` must be positive.
+
+ This function repeatedly computes subsequent partial sums until
+ convergence, as decided by ``tolerance``.
+ """
+ z = 0
+ z_prev = -float("inf")
+ k = 0
+ while abs(z - z_prev) > tolerance:
+ z_prev = z
+ z += 1 / ((k + q) ** x)
+ k += 1
+ return z
+
+
+def _generate_min_degree(gamma, average_degree, max_degree, tolerance, max_iters):
+ """Returns a minimum degree from the given average degree."""
+ # Defines zeta function whether or not Scipy is available
+ try:
+ from scipy.special import zeta
+ except ImportError:
+
+ def zeta(x, q):
+ return _hurwitz_zeta(x, q, tolerance)
+
+ min_deg_top = max_degree
+ min_deg_bot = 1
+ min_deg_mid = (min_deg_top - min_deg_bot) / 2 + min_deg_bot
+ itrs = 0
+ mid_avg_deg = 0
+ while abs(mid_avg_deg - average_degree) > tolerance:
+ if itrs > max_iters:
+ raise nx.ExceededMaxIterations("Could not match average_degree")
+ mid_avg_deg = 0
+ for x in range(int(min_deg_mid), max_degree + 1):
+ mid_avg_deg += (x ** (-gamma + 1)) / zeta(gamma, min_deg_mid)
+ if mid_avg_deg > average_degree:
+ min_deg_top = min_deg_mid
+ min_deg_mid = (min_deg_top - min_deg_bot) / 2 + min_deg_bot
+ else:
+ min_deg_bot = min_deg_mid
+ min_deg_mid = (min_deg_top - min_deg_bot) / 2 + min_deg_bot
+ itrs += 1
+ # return int(min_deg_mid + 0.5)
+ return round(min_deg_mid)
+
+
+def _generate_communities(degree_seq, community_sizes, mu, max_iters, seed):
+ """Returns a list of sets, each of which represents a community.
+
+ ``degree_seq`` is the degree sequence that must be met by the
+ graph.
+
+ ``community_sizes`` is the community size distribution that must be
+ met by the generated list of sets.
+
+ ``mu`` is a float in the interval [0, 1] indicating the fraction of
+ intra-community edges incident to each node.
+
+ ``max_iters`` is the number of times to try to add a node to a
+ community. This must be greater than the length of
+ ``degree_seq``, otherwise this function will always fail. If
+ the number of iterations exceeds this value,
+ :exc:`~networkx.exception.ExceededMaxIterations` is raised.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ The communities returned by this are sets of integers in the set {0,
+ ..., *n* - 1}, where *n* is the length of ``degree_seq``.
+
+ """
+ # This assumes the nodes in the graph will be natural numbers.
+ result = [set() for _ in community_sizes]
+ n = len(degree_seq)
+ free = list(range(n))
+ for i in range(max_iters):
+ v = free.pop()
+ c = seed.choice(range(len(community_sizes)))
+ # s = int(degree_seq[v] * (1 - mu) + 0.5)
+ s = round(degree_seq[v] * (1 - mu))
+ # If the community is large enough, add the node to the chosen
+ # community. Otherwise, return it to the list of unaffiliated
+ # nodes.
+ if s < community_sizes[c]:
+ result[c].add(v)
+ else:
+ free.append(v)
+ # If the community is too big, remove a node from it.
+ if len(result[c]) > community_sizes[c]:
+ free.append(result[c].pop())
+ if not free:
+ return result
+ msg = "Could not assign communities; try increasing min_community"
+ raise nx.ExceededMaxIterations(msg)
+
+
+@py_random_state(11)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def LFR_benchmark_graph(
+ n,
+ tau1,
+ tau2,
+ mu,
+ average_degree=None,
+ min_degree=None,
+ max_degree=None,
+ min_community=None,
+ max_community=None,
+ tol=1.0e-7,
+ max_iters=500,
+ seed=None,
+):
+ r"""Returns the LFR benchmark graph.
+
+ This algorithm proceeds as follows:
+
+ 1) Find a degree sequence with a power law distribution, and minimum
+ value ``min_degree``, which has approximate average degree
+ ``average_degree``. This is accomplished by either
+
+ a) specifying ``min_degree`` and not ``average_degree``,
+ b) specifying ``average_degree`` and not ``min_degree``, in which
+ case a suitable minimum degree will be found.
+
+ ``max_degree`` can also be specified, otherwise it will be set to
+ ``n``. Each node *u* will have $\mu \mathrm{deg}(u)$ edges
+ joining it to nodes in communities other than its own and $(1 -
+ \mu) \mathrm{deg}(u)$ edges joining it to nodes in its own
+ community.
+ 2) Generate community sizes according to a power law distribution
+ with exponent ``tau2``. If ``min_community`` and
+ ``max_community`` are not specified they will be selected to be
+ ``min_degree`` and ``max_degree``, respectively. Community sizes
+ are generated until the sum of their sizes equals ``n``.
+ 3) Each node will be randomly assigned a community with the
+ condition that the community is large enough for the node's
+ intra-community degree, $(1 - \mu) \mathrm{deg}(u)$ as
+ described in step 2. If a community grows too large, a random node
+ will be selected for reassignment to a new community, until all
+ nodes have been assigned a community.
+ 4) Each node *u* then adds $(1 - \mu) \mathrm{deg}(u)$
+ intra-community edges and $\mu \mathrm{deg}(u)$ inter-community
+ edges.
+
+ Parameters
+ ----------
+ n : int
+ Number of nodes in the created graph.
+
+ tau1 : float
+ Power law exponent for the degree distribution of the created
+ graph. This value must be strictly greater than one.
+
+ tau2 : float
+ Power law exponent for the community size distribution in the
+ created graph. This value must be strictly greater than one.
+
+ mu : float
+ Fraction of inter-community edges incident to each node. This
+ value must be in the interval [0, 1].
+
+ average_degree : float
+ Desired average degree of nodes in the created graph. This value
+ must be in the interval [0, *n*]. Exactly one of this and
+ ``min_degree`` must be specified, otherwise a
+ :exc:`NetworkXError` is raised.
+
+ min_degree : int
+ Minimum degree of nodes in the created graph. This value must be
+ in the interval [0, *n*]. Exactly one of this and
+ ``average_degree`` must be specified, otherwise a
+ :exc:`NetworkXError` is raised.
+
+ max_degree : int
+ Maximum degree of nodes in the created graph. If not specified,
+ this is set to ``n``, the total number of nodes in the graph.
+
+ min_community : int
+ Minimum size of communities in the graph. If not specified, this
+ is set to ``min_degree``.
+
+ max_community : int
+ Maximum size of communities in the graph. If not specified, this
+ is set to ``n``, the total number of nodes in the graph.
+
+ tol : float
+ Tolerance when comparing floats, specifically when comparing
+ average degree values.
+
+ max_iters : int
+ Maximum number of iterations to try to create the community sizes,
+ degree distribution, and community affiliations.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : NetworkX graph
+ The LFR benchmark graph generated according to the specified
+ parameters.
+
+ Each node in the graph has a node attribute ``'community'`` that
+ stores the community (that is, the set of nodes) that includes
+ it.
+
+ Raises
+ ------
+ NetworkXError
+ If any of the parameters do not meet their upper and lower bounds:
+
+ - ``tau1`` and ``tau2`` must be strictly greater than 1.
+ - ``mu`` must be in [0, 1].
+ - ``max_degree`` must be in {1, ..., *n*}.
+ - ``min_community`` and ``max_community`` must be in {0, ...,
+ *n*}.
+
+ If not exactly one of ``average_degree`` and ``min_degree`` is
+ specified.
+
+ If ``min_degree`` is not specified and a suitable ``min_degree``
+ cannot be found.
+
+ ExceededMaxIterations
+ If a valid degree sequence cannot be created within
+ ``max_iters`` number of iterations.
+
+ If a valid set of community sizes cannot be created within
+ ``max_iters`` number of iterations.
+
+ If a valid community assignment cannot be created within ``10 *
+ n * max_iters`` number of iterations.
+
+ Examples
+ --------
+ Basic usage::
+
+ >>> from networkx.generators.community import LFR_benchmark_graph
+ >>> n = 250
+ >>> tau1 = 3
+ >>> tau2 = 1.5
+ >>> mu = 0.1
+ >>> G = LFR_benchmark_graph(
+ ... n, tau1, tau2, mu, average_degree=5, min_community=20, seed=10
+ ... )
+
+ Continuing the example above, you can get the communities from the
+ node attributes of the graph::
+
+ >>> communities = {frozenset(G.nodes[v]["community"]) for v in G}
+
+ Notes
+ -----
+ This algorithm differs slightly from the original way it was
+ presented in [1].
+
+ 1) Rather than connecting the graph via a configuration model then
+ rewiring to match the intra-community and inter-community
+ degrees, we do this wiring explicitly at the end, which should be
+ equivalent.
+ 2) The code posted on the author's website [2] calculates the random
+ power law distributed variables and their average using
+ continuous approximations, whereas we use the discrete
+ distributions here as both degree and community size are
+ discrete.
+
+ Though the authors describe the algorithm as quite robust, testing
+ during development indicates that a somewhat narrower parameter set
+ is likely to successfully produce a graph. Some suggestions have
+ been provided in the event of exceptions.
+
+ References
+ ----------
+ .. [1] "Benchmark graphs for testing community detection algorithms",
+ Andrea Lancichinetti, Santo Fortunato, and Filippo Radicchi,
+ Phys. Rev. E 78, 046110 2008
+ .. [2] https://www.santofortunato.net/resources
+
+ """
+ # Perform some basic parameter validation.
+ if not tau1 > 1:
+ raise nx.NetworkXError("tau1 must be greater than one")
+ if not tau2 > 1:
+ raise nx.NetworkXError("tau2 must be greater than one")
+ if not 0 <= mu <= 1:
+ raise nx.NetworkXError("mu must be in the interval [0, 1]")
+
+ # Validate parameters for generating the degree sequence.
+ if max_degree is None:
+ max_degree = n
+ elif not 0 < max_degree <= n:
+ raise nx.NetworkXError("max_degree must be in the interval (0, n]")
+ if not ((min_degree is None) ^ (average_degree is None)):
+ raise nx.NetworkXError(
+ "Must assign exactly one of min_degree and average_degree"
+ )
+ if min_degree is None:
+ min_degree = _generate_min_degree(
+ tau1, average_degree, max_degree, tol, max_iters
+ )
+
+ # Generate a degree sequence with a power law distribution.
+ low, high = min_degree, max_degree
+
+ def condition(seq):
+ return sum(seq) % 2 == 0
+
+ def length(seq):
+ return len(seq) >= n
+
+ deg_seq = _powerlaw_sequence(tau1, low, high, condition, length, max_iters, seed)
+
+ # Validate parameters for generating the community size sequence.
+ if min_community is None:
+ min_community = min(deg_seq)
+ if max_community is None:
+ max_community = max(deg_seq)
+
+ # Generate a community size sequence with a power law distribution.
+ #
+ # TODO The original code incremented the number of iterations each
+ # time a new Zipf random value was drawn from the distribution. This
+ # differed from the way the number of iterations was incremented in
+ # `_powerlaw_degree_sequence`, so this code was changed to match
+ # that one. As a result, this code is allowed many more chances to
+ # generate a valid community size sequence.
+ low, high = min_community, max_community
+
+ def condition(seq):
+ return sum(seq) == n
+
+ def length(seq):
+ return sum(seq) >= n
+
+ comms = _powerlaw_sequence(tau2, low, high, condition, length, max_iters, seed)
+
+ # Generate the communities based on the given degree sequence and
+ # community sizes.
+ max_iters *= 10 * n
+ communities = _generate_communities(deg_seq, comms, mu, max_iters, seed)
+
+ # Finally, generate the benchmark graph based on the given
+ # communities, joining nodes according to the intra- and
+ # inter-community degrees.
+ G = nx.Graph()
+ G.add_nodes_from(range(n))
+ for c in communities:
+ for u in c:
+ while G.degree(u) < round(deg_seq[u] * (1 - mu)):
+ v = seed.choice(list(c))
+ G.add_edge(u, v)
+ while G.degree(u) < deg_seq[u]:
+ v = seed.choice(range(n))
+ if v not in c:
+ G.add_edge(u, v)
+ G.nodes[u]["community"] = c
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/degree_seq.py b/.venv/lib/python3.12/site-packages/networkx/generators/degree_seq.py
new file mode 100644
index 00000000..a27dd22e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/degree_seq.py
@@ -0,0 +1,867 @@
+"""Generate graphs with a given degree sequence or expected degree sequence."""
+
+import heapq
+import math
+from itertools import chain, combinations, zip_longest
+from operator import itemgetter
+
+import networkx as nx
+from networkx.utils import py_random_state, random_weighted_sample
+
+__all__ = [
+ "configuration_model",
+ "directed_configuration_model",
+ "expected_degree_graph",
+ "havel_hakimi_graph",
+ "directed_havel_hakimi_graph",
+ "degree_sequence_tree",
+ "random_degree_sequence_graph",
+]
+
+chaini = chain.from_iterable
+
+
+def _to_stublist(degree_sequence):
+ """Returns a list of degree-repeated node numbers.
+
+ ``degree_sequence`` is a list of nonnegative integers representing
+ the degrees of nodes in a graph.
+
+ This function returns a list of node numbers with multiplicities
+ according to the given degree sequence. For example, if the first
+ element of ``degree_sequence`` is ``3``, then the first node number,
+ ``0``, will appear at the head of the returned list three times. The
+ node numbers are assumed to be the numbers zero through
+ ``len(degree_sequence) - 1``.
+
+ Examples
+ --------
+
+ >>> degree_sequence = [1, 2, 3]
+ >>> _to_stublist(degree_sequence)
+ [0, 1, 1, 2, 2, 2]
+
+ If a zero appears in the sequence, that means the node exists but
+ has degree zero, so that number will be skipped in the returned
+ list::
+
+ >>> degree_sequence = [2, 0, 1]
+ >>> _to_stublist(degree_sequence)
+ [0, 0, 2]
+
+ """
+ return list(chaini([n] * d for n, d in enumerate(degree_sequence)))
+
+
+def _configuration_model(
+ deg_sequence, create_using, directed=False, in_deg_sequence=None, seed=None
+):
+ """Helper function for generating either undirected or directed
+ configuration model graphs.
+
+ ``deg_sequence`` is a list of nonnegative integers representing the
+ degree of the node whose label is the index of the list element.
+
+ ``create_using`` see :func:`~networkx.empty_graph`.
+
+ ``directed`` and ``in_deg_sequence`` are required if you want the
+ returned graph to be generated using the directed configuration
+ model algorithm. If ``directed`` is ``False``, then ``deg_sequence``
+ is interpreted as the degree sequence of an undirected graph and
+ ``in_deg_sequence`` is ignored. Otherwise, if ``directed`` is
+ ``True``, then ``deg_sequence`` is interpreted as the out-degree
+ sequence and ``in_deg_sequence`` as the in-degree sequence of a
+ directed graph.
+
+ .. note::
+
+ ``deg_sequence`` and ``in_deg_sequence`` need not be the same
+ length.
+
+ ``seed`` is a random.Random or numpy.random.RandomState instance
+
+ This function returns a graph, directed if and only if ``directed``
+ is ``True``, generated according to the configuration model
+ algorithm. For more information on the algorithm, see the
+ :func:`configuration_model` or :func:`directed_configuration_model`
+ functions.
+
+ """
+ n = len(deg_sequence)
+ G = nx.empty_graph(n, create_using)
+ # If empty, return the null graph immediately.
+ if n == 0:
+ return G
+ # Build a list of available degree-repeated nodes. For example,
+ # for degree sequence [3, 2, 1, 1, 1], the "stub list" is
+ # initially [0, 0, 0, 1, 1, 2, 3, 4], that is, node 0 has degree
+ # 3 and thus is repeated 3 times, etc.
+ #
+ # Also, shuffle the stub list in order to get a random sequence of
+ # node pairs.
+ if directed:
+ pairs = zip_longest(deg_sequence, in_deg_sequence, fillvalue=0)
+ # Unzip the list of pairs into a pair of lists.
+ out_deg, in_deg = zip(*pairs)
+
+ out_stublist = _to_stublist(out_deg)
+ in_stublist = _to_stublist(in_deg)
+
+ seed.shuffle(out_stublist)
+ seed.shuffle(in_stublist)
+ else:
+ stublist = _to_stublist(deg_sequence)
+ # Choose a random balanced bipartition of the stublist, which
+ # gives a random pairing of nodes. In this implementation, we
+ # shuffle the list and then split it in half.
+ n = len(stublist)
+ half = n // 2
+ seed.shuffle(stublist)
+ out_stublist, in_stublist = stublist[:half], stublist[half:]
+ G.add_edges_from(zip(out_stublist, in_stublist))
+ return G
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def configuration_model(deg_sequence, create_using=None, seed=None):
+ """Returns a random graph with the given degree sequence.
+
+ The configuration model generates a random pseudograph (graph with
+ parallel edges and self loops) by randomly assigning edges to
+ match the given degree sequence.
+
+ Parameters
+ ----------
+ deg_sequence : list of nonnegative integers
+ Each list entry corresponds to the degree of a node.
+ create_using : NetworkX graph constructor, optional (default MultiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : MultiGraph
+ A graph with the specified degree sequence.
+ Nodes are labeled starting at 0 with an index
+ corresponding to the position in deg_sequence.
+
+ Raises
+ ------
+ NetworkXError
+ If the degree sequence does not have an even sum.
+
+ See Also
+ --------
+ is_graphical
+
+ Notes
+ -----
+ As described by Newman [1]_.
+
+ A non-graphical degree sequence (not realizable by some simple
+ graph) is allowed since this function returns graphs with self
+ loops and parallel edges. An exception is raised if the degree
+ sequence does not have an even sum.
+
+ This configuration model construction process can lead to
+ duplicate edges and loops. You can remove the self-loops and
+ parallel edges (see below) which will likely result in a graph
+ that doesn't have the exact degree sequence specified.
+
+ The density of self-loops and parallel edges tends to decrease as
+ the number of nodes increases. However, typically the number of
+ self-loops will approach a Poisson distribution with a nonzero mean,
+ and similarly for the number of parallel edges. Consider a node
+ with *k* stubs. The probability of being joined to another stub of
+ the same node is basically (*k* - *1*) / *N*, where *k* is the
+ degree and *N* is the number of nodes. So the probability of a
+ self-loop scales like *c* / *N* for some constant *c*. As *N* grows,
+ this means we expect *c* self-loops. Similarly for parallel edges.
+
+ References
+ ----------
+ .. [1] M.E.J. Newman, "The structure and function of complex networks",
+ SIAM REVIEW 45-2, pp 167-256, 2003.
+
+ Examples
+ --------
+ You can create a degree sequence following a particular distribution
+ by using the one of the distribution functions in
+ :mod:`~networkx.utils.random_sequence` (or one of your own). For
+ example, to create an undirected multigraph on one hundred nodes
+ with degree sequence chosen from the power law distribution:
+
+ >>> sequence = nx.random_powerlaw_tree_sequence(100, tries=5000)
+ >>> G = nx.configuration_model(sequence)
+ >>> len(G)
+ 100
+ >>> actual_degrees = [d for v, d in G.degree()]
+ >>> actual_degrees == sequence
+ True
+
+ The returned graph is a multigraph, which may have parallel
+ edges. To remove any parallel edges from the returned graph:
+
+ >>> G = nx.Graph(G)
+
+ Similarly, to remove self-loops:
+
+ >>> G.remove_edges_from(nx.selfloop_edges(G))
+
+ """
+ if sum(deg_sequence) % 2 != 0:
+ msg = "Invalid degree sequence: sum of degrees must be even, not odd"
+ raise nx.NetworkXError(msg)
+
+ G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
+ if G.is_directed():
+ raise nx.NetworkXNotImplemented("not implemented for directed graphs")
+
+ G = _configuration_model(deg_sequence, G, seed=seed)
+
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def directed_configuration_model(
+ in_degree_sequence, out_degree_sequence, create_using=None, seed=None
+):
+ """Returns a directed_random graph with the given degree sequences.
+
+ The configuration model generates a random directed pseudograph
+ (graph with parallel edges and self loops) by randomly assigning
+ edges to match the given degree sequences.
+
+ Parameters
+ ----------
+ in_degree_sequence : list of nonnegative integers
+ Each list entry corresponds to the in-degree of a node.
+ out_degree_sequence : list of nonnegative integers
+ Each list entry corresponds to the out-degree of a node.
+ create_using : NetworkX graph constructor, optional (default MultiDiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : MultiDiGraph
+ A graph with the specified degree sequences.
+ Nodes are labeled starting at 0 with an index
+ corresponding to the position in deg_sequence.
+
+ Raises
+ ------
+ NetworkXError
+ If the degree sequences do not have the same sum.
+
+ See Also
+ --------
+ configuration_model
+
+ Notes
+ -----
+ Algorithm as described by Newman [1]_.
+
+ A non-graphical degree sequence (not realizable by some simple
+ graph) is allowed since this function returns graphs with self
+ loops and parallel edges. An exception is raised if the degree
+ sequences does not have the same sum.
+
+ This configuration model construction process can lead to
+ duplicate edges and loops. You can remove the self-loops and
+ parallel edges (see below) which will likely result in a graph
+ that doesn't have the exact degree sequence specified. This
+ "finite-size effect" decreases as the size of the graph increases.
+
+ References
+ ----------
+ .. [1] Newman, M. E. J. and Strogatz, S. H. and Watts, D. J.
+ Random graphs with arbitrary degree distributions and their applications
+ Phys. Rev. E, 64, 026118 (2001)
+
+ Examples
+ --------
+ One can modify the in- and out-degree sequences from an existing
+ directed graph in order to create a new directed graph. For example,
+ here we modify the directed path graph:
+
+ >>> D = nx.DiGraph([(0, 1), (1, 2), (2, 3)])
+ >>> din = list(d for n, d in D.in_degree())
+ >>> dout = list(d for n, d in D.out_degree())
+ >>> din.append(1)
+ >>> dout[0] = 2
+ >>> # We now expect an edge from node 0 to a new node, node 3.
+ ... D = nx.directed_configuration_model(din, dout)
+
+ The returned graph is a directed multigraph, which may have parallel
+ edges. To remove any parallel edges from the returned graph:
+
+ >>> D = nx.DiGraph(D)
+
+ Similarly, to remove self-loops:
+
+ >>> D.remove_edges_from(nx.selfloop_edges(D))
+
+ """
+ if sum(in_degree_sequence) != sum(out_degree_sequence):
+ msg = "Invalid degree sequences: sequences must have equal sums"
+ raise nx.NetworkXError(msg)
+
+ if create_using is None:
+ create_using = nx.MultiDiGraph
+
+ G = _configuration_model(
+ out_degree_sequence,
+ create_using,
+ directed=True,
+ in_deg_sequence=in_degree_sequence,
+ seed=seed,
+ )
+
+ name = "directed configuration_model {} nodes {} edges"
+ return G
+
+
+@py_random_state(1)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def expected_degree_graph(w, seed=None, selfloops=True):
+ r"""Returns a random graph with given expected degrees.
+
+ Given a sequence of expected degrees $W=(w_0,w_1,\ldots,w_{n-1})$
+ of length $n$ this algorithm assigns an edge between node $u$ and
+ node $v$ with probability
+
+ .. math::
+
+ p_{uv} = \frac{w_u w_v}{\sum_k w_k} .
+
+ Parameters
+ ----------
+ w : list
+ The list of expected degrees.
+ selfloops: bool (default=True)
+ Set to False to remove the possibility of self-loop edges.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ Graph
+
+ Examples
+ --------
+ >>> z = [10 for i in range(100)]
+ >>> G = nx.expected_degree_graph(z)
+
+ Notes
+ -----
+ The nodes have integer labels corresponding to index of expected degrees
+ input sequence.
+
+ The complexity of this algorithm is $\mathcal{O}(n+m)$ where $n$ is the
+ number of nodes and $m$ is the expected number of edges.
+
+ The model in [1]_ includes the possibility of self-loop edges.
+ Set selfloops=False to produce a graph without self loops.
+
+ For finite graphs this model doesn't produce exactly the given
+ expected degree sequence. Instead the expected degrees are as
+ follows.
+
+ For the case without self loops (selfloops=False),
+
+ .. math::
+
+ E[deg(u)] = \sum_{v \ne u} p_{uv}
+ = w_u \left( 1 - \frac{w_u}{\sum_k w_k} \right) .
+
+
+ NetworkX uses the standard convention that a self-loop edge counts 2
+ in the degree of a node, so with self loops (selfloops=True),
+
+ .. math::
+
+ E[deg(u)] = \sum_{v \ne u} p_{uv} + 2 p_{uu}
+ = w_u \left( 1 + \frac{w_u}{\sum_k w_k} \right) .
+
+ References
+ ----------
+ .. [1] Fan Chung and L. Lu, Connected components in random graphs with
+ given expected degree sequences, Ann. Combinatorics, 6,
+ pp. 125-145, 2002.
+ .. [2] Joel Miller and Aric Hagberg,
+ Efficient generation of networks with given expected degrees,
+ in Algorithms and Models for the Web-Graph (WAW 2011),
+ Alan Frieze, Paul Horn, and Paweł Prałat (Eds), LNCS 6732,
+ pp. 115-126, 2011.
+ """
+ n = len(w)
+ G = nx.empty_graph(n)
+
+ # If there are no nodes are no edges in the graph, return the empty graph.
+ if n == 0 or max(w) == 0:
+ return G
+
+ rho = 1 / sum(w)
+ # Sort the weights in decreasing order. The original order of the
+ # weights dictates the order of the (integer) node labels, so we
+ # need to remember the permutation applied in the sorting.
+ order = sorted(enumerate(w), key=itemgetter(1), reverse=True)
+ mapping = {c: u for c, (u, v) in enumerate(order)}
+ seq = [v for u, v in order]
+ last = n
+ if not selfloops:
+ last -= 1
+ for u in range(last):
+ v = u
+ if not selfloops:
+ v += 1
+ factor = seq[u] * rho
+ p = min(seq[v] * factor, 1)
+ while v < n and p > 0:
+ if p != 1:
+ r = seed.random()
+ v += math.floor(math.log(r, 1 - p))
+ if v < n:
+ q = min(seq[v] * factor, 1)
+ if seed.random() < q / p:
+ G.add_edge(mapping[u], mapping[v])
+ v += 1
+ p = q
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def havel_hakimi_graph(deg_sequence, create_using=None):
+ """Returns a simple graph with given degree sequence constructed
+ using the Havel-Hakimi algorithm.
+
+ Parameters
+ ----------
+ deg_sequence: list of integers
+ Each integer corresponds to the degree of a node (need not be sorted).
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Directed graphs are not allowed.
+
+ Raises
+ ------
+ NetworkXException
+ For a non-graphical degree sequence (i.e. one
+ not realizable by some simple graph).
+
+ Notes
+ -----
+ The Havel-Hakimi algorithm constructs a simple graph by
+ successively connecting the node of highest degree to other nodes
+ of highest degree, resorting remaining nodes by degree, and
+ repeating the process. The resulting graph has a high
+ degree-associativity. Nodes are labeled 1,.., len(deg_sequence),
+ corresponding to their position in deg_sequence.
+
+ The basic algorithm is from Hakimi [1]_ and was generalized by
+ Kleitman and Wang [2]_.
+
+ References
+ ----------
+ .. [1] Hakimi S., On Realizability of a Set of Integers as
+ Degrees of the Vertices of a Linear Graph. I,
+ Journal of SIAM, 10(3), pp. 496-506 (1962)
+ .. [2] Kleitman D.J. and Wang D.L.
+ Algorithms for Constructing Graphs and Digraphs with Given Valences
+ and Factors Discrete Mathematics, 6(1), pp. 79-88 (1973)
+ """
+ if not nx.is_graphical(deg_sequence):
+ raise nx.NetworkXError("Invalid degree sequence")
+
+ p = len(deg_sequence)
+ G = nx.empty_graph(p, create_using)
+ if G.is_directed():
+ raise nx.NetworkXError("Directed graphs are not supported")
+ num_degs = [[] for i in range(p)]
+ dmax, dsum, n = 0, 0, 0
+ for d in deg_sequence:
+ # Process only the non-zero integers
+ if d > 0:
+ num_degs[d].append(n)
+ dmax, dsum, n = max(dmax, d), dsum + d, n + 1
+ # Return graph if no edges
+ if n == 0:
+ return G
+
+ modstubs = [(0, 0)] * (dmax + 1)
+ # Successively reduce degree sequence by removing the maximum degree
+ while n > 0:
+ # Retrieve the maximum degree in the sequence
+ while len(num_degs[dmax]) == 0:
+ dmax -= 1
+ # If there are not enough stubs to connect to, then the sequence is
+ # not graphical
+ if dmax > n - 1:
+ raise nx.NetworkXError("Non-graphical integer sequence")
+
+ # Remove largest stub in list
+ source = num_degs[dmax].pop()
+ n -= 1
+ # Reduce the next dmax largest stubs
+ mslen = 0
+ k = dmax
+ for i in range(dmax):
+ while len(num_degs[k]) == 0:
+ k -= 1
+ target = num_degs[k].pop()
+ G.add_edge(source, target)
+ n -= 1
+ if k > 1:
+ modstubs[mslen] = (k - 1, target)
+ mslen += 1
+ # Add back to the list any nonzero stubs that were removed
+ for i in range(mslen):
+ (stubval, stubtarget) = modstubs[i]
+ num_degs[stubval].append(stubtarget)
+ n += 1
+
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def directed_havel_hakimi_graph(in_deg_sequence, out_deg_sequence, create_using=None):
+ """Returns a directed graph with the given degree sequences.
+
+ Parameters
+ ----------
+ in_deg_sequence : list of integers
+ Each list entry corresponds to the in-degree of a node.
+ out_deg_sequence : list of integers
+ Each list entry corresponds to the out-degree of a node.
+ create_using : NetworkX graph constructor, optional (default DiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : DiGraph
+ A graph with the specified degree sequences.
+ Nodes are labeled starting at 0 with an index
+ corresponding to the position in deg_sequence
+
+ Raises
+ ------
+ NetworkXError
+ If the degree sequences are not digraphical.
+
+ See Also
+ --------
+ configuration_model
+
+ Notes
+ -----
+ Algorithm as described by Kleitman and Wang [1]_.
+
+ References
+ ----------
+ .. [1] D.J. Kleitman and D.L. Wang
+ Algorithms for Constructing Graphs and Digraphs with Given Valences
+ and Factors Discrete Mathematics, 6(1), pp. 79-88 (1973)
+ """
+ in_deg_sequence = nx.utils.make_list_of_ints(in_deg_sequence)
+ out_deg_sequence = nx.utils.make_list_of_ints(out_deg_sequence)
+
+ # Process the sequences and form two heaps to store degree pairs with
+ # either zero or nonzero out degrees
+ sumin, sumout = 0, 0
+ nin, nout = len(in_deg_sequence), len(out_deg_sequence)
+ maxn = max(nin, nout)
+ G = nx.empty_graph(maxn, create_using, default=nx.DiGraph)
+ if maxn == 0:
+ return G
+ maxin = 0
+ stubheap, zeroheap = [], []
+ for n in range(maxn):
+ in_deg, out_deg = 0, 0
+ if n < nout:
+ out_deg = out_deg_sequence[n]
+ if n < nin:
+ in_deg = in_deg_sequence[n]
+ if in_deg < 0 or out_deg < 0:
+ raise nx.NetworkXError(
+ "Invalid degree sequences. Sequence values must be positive."
+ )
+ sumin, sumout, maxin = sumin + in_deg, sumout + out_deg, max(maxin, in_deg)
+ if in_deg > 0:
+ stubheap.append((-1 * out_deg, -1 * in_deg, n))
+ elif out_deg > 0:
+ zeroheap.append((-1 * out_deg, n))
+ if sumin != sumout:
+ raise nx.NetworkXError(
+ "Invalid degree sequences. Sequences must have equal sums."
+ )
+ heapq.heapify(stubheap)
+ heapq.heapify(zeroheap)
+
+ modstubs = [(0, 0, 0)] * (maxin + 1)
+ # Successively reduce degree sequence by removing the maximum
+ while stubheap:
+ # Remove first value in the sequence with a non-zero in degree
+ (freeout, freein, target) = heapq.heappop(stubheap)
+ freein *= -1
+ if freein > len(stubheap) + len(zeroheap):
+ raise nx.NetworkXError("Non-digraphical integer sequence")
+
+ # Attach arcs from the nodes with the most stubs
+ mslen = 0
+ for i in range(freein):
+ if zeroheap and (not stubheap or stubheap[0][0] > zeroheap[0][0]):
+ (stubout, stubsource) = heapq.heappop(zeroheap)
+ stubin = 0
+ else:
+ (stubout, stubin, stubsource) = heapq.heappop(stubheap)
+ if stubout == 0:
+ raise nx.NetworkXError("Non-digraphical integer sequence")
+ G.add_edge(stubsource, target)
+ # Check if source is now totally connected
+ if stubout + 1 < 0 or stubin < 0:
+ modstubs[mslen] = (stubout + 1, stubin, stubsource)
+ mslen += 1
+
+ # Add the nodes back to the heaps that still have available stubs
+ for i in range(mslen):
+ stub = modstubs[i]
+ if stub[1] < 0:
+ heapq.heappush(stubheap, stub)
+ else:
+ heapq.heappush(zeroheap, (stub[0], stub[2]))
+ if freeout < 0:
+ heapq.heappush(zeroheap, (freeout, target))
+
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def degree_sequence_tree(deg_sequence, create_using=None):
+ """Make a tree for the given degree sequence.
+
+ A tree has #nodes-#edges=1 so
+ the degree sequence must have
+ len(deg_sequence)-sum(deg_sequence)/2=1
+ """
+ # The sum of the degree sequence must be even (for any undirected graph).
+ degree_sum = sum(deg_sequence)
+ if degree_sum % 2 != 0:
+ msg = "Invalid degree sequence: sum of degrees must be even, not odd"
+ raise nx.NetworkXError(msg)
+ if len(deg_sequence) - degree_sum // 2 != 1:
+ msg = (
+ "Invalid degree sequence: tree must have number of nodes equal"
+ " to one less than the number of edges"
+ )
+ raise nx.NetworkXError(msg)
+ G = nx.empty_graph(0, create_using)
+ if G.is_directed():
+ raise nx.NetworkXError("Directed Graph not supported")
+
+ # Sort all degrees greater than 1 in decreasing order.
+ #
+ # TODO Does this need to be sorted in reverse order?
+ deg = sorted((s for s in deg_sequence if s > 1), reverse=True)
+
+ # make path graph as backbone
+ n = len(deg) + 2
+ nx.add_path(G, range(n))
+ last = n
+
+ # add the leaves
+ for source in range(1, n - 1):
+ nedges = deg.pop() - 2
+ for target in range(last, last + nedges):
+ G.add_edge(source, target)
+ last += nedges
+
+ # in case we added one too many
+ if len(G) > len(deg_sequence):
+ G.remove_node(0)
+ return G
+
+
+@py_random_state(1)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_degree_sequence_graph(sequence, seed=None, tries=10):
+ r"""Returns a simple random graph with the given degree sequence.
+
+ If the maximum degree $d_m$ in the sequence is $O(m^{1/4})$ then the
+ algorithm produces almost uniform random graphs in $O(m d_m)$ time
+ where $m$ is the number of edges.
+
+ Parameters
+ ----------
+ sequence : list of integers
+ Sequence of degrees
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ tries : int, optional
+ Maximum number of tries to create a graph
+
+ Returns
+ -------
+ G : Graph
+ A graph with the specified degree sequence.
+ Nodes are labeled starting at 0 with an index
+ corresponding to the position in the sequence.
+
+ Raises
+ ------
+ NetworkXUnfeasible
+ If the degree sequence is not graphical.
+ NetworkXError
+ If a graph is not produced in specified number of tries
+
+ See Also
+ --------
+ is_graphical, configuration_model
+
+ Notes
+ -----
+ The generator algorithm [1]_ is not guaranteed to produce a graph.
+
+ References
+ ----------
+ .. [1] Moshen Bayati, Jeong Han Kim, and Amin Saberi,
+ A sequential algorithm for generating random graphs.
+ Algorithmica, Volume 58, Number 4, 860-910,
+ DOI: 10.1007/s00453-009-9340-1
+
+ Examples
+ --------
+ >>> sequence = [1, 2, 2, 3]
+ >>> G = nx.random_degree_sequence_graph(sequence, seed=42)
+ >>> sorted(d for n, d in G.degree())
+ [1, 2, 2, 3]
+ """
+ DSRG = DegreeSequenceRandomGraph(sequence, seed)
+ for try_n in range(tries):
+ try:
+ return DSRG.generate()
+ except nx.NetworkXUnfeasible:
+ pass
+ raise nx.NetworkXError(f"failed to generate graph in {tries} tries")
+
+
+class DegreeSequenceRandomGraph:
+ # class to generate random graphs with a given degree sequence
+ # use random_degree_sequence_graph()
+ def __init__(self, degree, rng):
+ if not nx.is_graphical(degree):
+ raise nx.NetworkXUnfeasible("degree sequence is not graphical")
+ self.rng = rng
+ self.degree = list(degree)
+ # node labels are integers 0,...,n-1
+ self.m = sum(self.degree) / 2.0 # number of edges
+ try:
+ self.dmax = max(self.degree) # maximum degree
+ except ValueError:
+ self.dmax = 0
+
+ def generate(self):
+ # remaining_degree is mapping from int->remaining degree
+ self.remaining_degree = dict(enumerate(self.degree))
+ # add all nodes to make sure we get isolated nodes
+ self.graph = nx.Graph()
+ self.graph.add_nodes_from(self.remaining_degree)
+ # remove zero degree nodes
+ for n, d in list(self.remaining_degree.items()):
+ if d == 0:
+ del self.remaining_degree[n]
+ if len(self.remaining_degree) > 0:
+ # build graph in three phases according to how many unmatched edges
+ self.phase1()
+ self.phase2()
+ self.phase3()
+ return self.graph
+
+ def update_remaining(self, u, v, aux_graph=None):
+ # decrement remaining nodes, modify auxiliary graph if in phase3
+ if aux_graph is not None:
+ # remove edges from auxiliary graph
+ aux_graph.remove_edge(u, v)
+ if self.remaining_degree[u] == 1:
+ del self.remaining_degree[u]
+ if aux_graph is not None:
+ aux_graph.remove_node(u)
+ else:
+ self.remaining_degree[u] -= 1
+ if self.remaining_degree[v] == 1:
+ del self.remaining_degree[v]
+ if aux_graph is not None:
+ aux_graph.remove_node(v)
+ else:
+ self.remaining_degree[v] -= 1
+
+ def p(self, u, v):
+ # degree probability
+ return 1 - self.degree[u] * self.degree[v] / (4.0 * self.m)
+
+ def q(self, u, v):
+ # remaining degree probability
+ norm = max(self.remaining_degree.values()) ** 2
+ return self.remaining_degree[u] * self.remaining_degree[v] / norm
+
+ def suitable_edge(self):
+ """Returns True if and only if an arbitrary remaining node can
+ potentially be joined with some other remaining node.
+
+ """
+ nodes = iter(self.remaining_degree)
+ u = next(nodes)
+ return any(v not in self.graph[u] for v in nodes)
+
+ def phase1(self):
+ # choose node pairs from (degree) weighted distribution
+ rem_deg = self.remaining_degree
+ while sum(rem_deg.values()) >= 2 * self.dmax**2:
+ u, v = sorted(random_weighted_sample(rem_deg, 2, self.rng))
+ if self.graph.has_edge(u, v):
+ continue
+ if self.rng.random() < self.p(u, v): # accept edge
+ self.graph.add_edge(u, v)
+ self.update_remaining(u, v)
+
+ def phase2(self):
+ # choose remaining nodes uniformly at random and use rejection sampling
+ remaining_deg = self.remaining_degree
+ rng = self.rng
+ while len(remaining_deg) >= 2 * self.dmax:
+ while True:
+ u, v = sorted(rng.sample(list(remaining_deg.keys()), 2))
+ if self.graph.has_edge(u, v):
+ continue
+ if rng.random() < self.q(u, v):
+ break
+ if rng.random() < self.p(u, v): # accept edge
+ self.graph.add_edge(u, v)
+ self.update_remaining(u, v)
+
+ def phase3(self):
+ # build potential remaining edges and choose with rejection sampling
+ potential_edges = combinations(self.remaining_degree, 2)
+ # build auxiliary graph of potential edges not already in graph
+ H = nx.Graph(
+ [(u, v) for (u, v) in potential_edges if not self.graph.has_edge(u, v)]
+ )
+ rng = self.rng
+ while self.remaining_degree:
+ if not self.suitable_edge():
+ raise nx.NetworkXUnfeasible("no suitable edges left")
+ while True:
+ u, v = sorted(rng.choice(list(H.edges())))
+ if rng.random() < self.q(u, v):
+ break
+ if rng.random() < self.p(u, v): # accept edge
+ self.graph.add_edge(u, v)
+ self.update_remaining(u, v, aux_graph=H)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/directed.py b/.venv/lib/python3.12/site-packages/networkx/generators/directed.py
new file mode 100644
index 00000000..4548726b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/directed.py
@@ -0,0 +1,501 @@
+"""
+Generators for some directed graphs, including growing network (GN) graphs and
+scale-free graphs.
+
+"""
+
+import numbers
+from collections import Counter
+
+import networkx as nx
+from networkx.generators.classic import empty_graph
+from networkx.utils import discrete_sequence, py_random_state, weighted_choice
+
+__all__ = [
+ "gn_graph",
+ "gnc_graph",
+ "gnr_graph",
+ "random_k_out_graph",
+ "scale_free_graph",
+]
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def gn_graph(n, kernel=None, create_using=None, seed=None):
+ """Returns the growing network (GN) digraph with `n` nodes.
+
+ The GN graph is built by adding nodes one at a time with a link to one
+ previously added node. The target node for the link is chosen with
+ probability based on degree. The default attachment kernel is a linear
+ function of the degree of a node.
+
+ The graph is always a (directed) tree.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes for the generated graph.
+ kernel : function
+ The attachment kernel.
+ create_using : NetworkX graph constructor, optional (default DiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Examples
+ --------
+ To create the undirected GN graph, use the :meth:`~DiGraph.to_directed`
+ method::
+
+ >>> D = nx.gn_graph(10) # the GN graph
+ >>> G = D.to_undirected() # the undirected version
+
+ To specify an attachment kernel, use the `kernel` keyword argument::
+
+ >>> D = nx.gn_graph(10, kernel=lambda x: x**1.5) # A_k = k^1.5
+
+ References
+ ----------
+ .. [1] P. L. Krapivsky and S. Redner,
+ Organization of Growing Random Networks,
+ Phys. Rev. E, 63, 066123, 2001.
+ """
+ G = empty_graph(1, create_using, default=nx.DiGraph)
+ if not G.is_directed():
+ raise nx.NetworkXError("create_using must indicate a Directed Graph")
+
+ if kernel is None:
+
+ def kernel(x):
+ return x
+
+ if n == 1:
+ return G
+
+ G.add_edge(1, 0) # get started
+ ds = [1, 1] # degree sequence
+
+ for source in range(2, n):
+ # compute distribution from kernel and degree
+ dist = [kernel(d) for d in ds]
+ # choose target from discrete distribution
+ target = discrete_sequence(1, distribution=dist, seed=seed)[0]
+ G.add_edge(source, target)
+ ds.append(1) # the source has only one link (degree one)
+ ds[target] += 1 # add one to the target link degree
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def gnr_graph(n, p, create_using=None, seed=None):
+ """Returns the growing network with redirection (GNR) digraph with `n`
+ nodes and redirection probability `p`.
+
+ The GNR graph is built by adding nodes one at a time with a link to one
+ previously added node. The previous target node is chosen uniformly at
+ random. With probability `p` the link is instead "redirected" to the
+ successor node of the target.
+
+ The graph is always a (directed) tree.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes for the generated graph.
+ p : float
+ The redirection probability.
+ create_using : NetworkX graph constructor, optional (default DiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Examples
+ --------
+ To create the undirected GNR graph, use the :meth:`~DiGraph.to_directed`
+ method::
+
+ >>> D = nx.gnr_graph(10, 0.5) # the GNR graph
+ >>> G = D.to_undirected() # the undirected version
+
+ References
+ ----------
+ .. [1] P. L. Krapivsky and S. Redner,
+ Organization of Growing Random Networks,
+ Phys. Rev. E, 63, 066123, 2001.
+ """
+ G = empty_graph(1, create_using, default=nx.DiGraph)
+ if not G.is_directed():
+ raise nx.NetworkXError("create_using must indicate a Directed Graph")
+
+ if n == 1:
+ return G
+
+ for source in range(1, n):
+ target = seed.randrange(0, source)
+ if seed.random() < p and target != 0:
+ target = next(G.successors(target))
+ G.add_edge(source, target)
+ return G
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def gnc_graph(n, create_using=None, seed=None):
+ """Returns the growing network with copying (GNC) digraph with `n` nodes.
+
+ The GNC graph is built by adding nodes one at a time with a link to one
+ previously added node (chosen uniformly at random) and to all of that
+ node's successors.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes for the generated graph.
+ create_using : NetworkX graph constructor, optional (default DiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ References
+ ----------
+ .. [1] P. L. Krapivsky and S. Redner,
+ Network Growth by Copying,
+ Phys. Rev. E, 71, 036118, 2005k.},
+ """
+ G = empty_graph(1, create_using, default=nx.DiGraph)
+ if not G.is_directed():
+ raise nx.NetworkXError("create_using must indicate a Directed Graph")
+
+ if n == 1:
+ return G
+
+ for source in range(1, n):
+ target = seed.randrange(0, source)
+ for succ in G.successors(target):
+ G.add_edge(source, succ)
+ G.add_edge(source, target)
+ return G
+
+
+@py_random_state(6)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def scale_free_graph(
+ n,
+ alpha=0.41,
+ beta=0.54,
+ gamma=0.05,
+ delta_in=0.2,
+ delta_out=0,
+ seed=None,
+ initial_graph=None,
+):
+ """Returns a scale-free directed graph.
+
+ Parameters
+ ----------
+ n : integer
+ Number of nodes in graph
+ alpha : float
+ Probability for adding a new node connected to an existing node
+ chosen randomly according to the in-degree distribution.
+ beta : float
+ Probability for adding an edge between two existing nodes.
+ One existing node is chosen randomly according the in-degree
+ distribution and the other chosen randomly according to the out-degree
+ distribution.
+ gamma : float
+ Probability for adding a new node connected to an existing node
+ chosen randomly according to the out-degree distribution.
+ delta_in : float
+ Bias for choosing nodes from in-degree distribution.
+ delta_out : float
+ Bias for choosing nodes from out-degree distribution.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ initial_graph : MultiDiGraph instance, optional
+ Build the scale-free graph starting from this initial MultiDiGraph,
+ if provided.
+
+ Returns
+ -------
+ MultiDiGraph
+
+ Examples
+ --------
+ Create a scale-free graph on one hundred nodes::
+
+ >>> G = nx.scale_free_graph(100)
+
+ Notes
+ -----
+ The sum of `alpha`, `beta`, and `gamma` must be 1.
+
+ References
+ ----------
+ .. [1] B. Bollobás, C. Borgs, J. Chayes, and O. Riordan,
+ Directed scale-free graphs,
+ Proceedings of the fourteenth annual ACM-SIAM Symposium on
+ Discrete Algorithms, 132--139, 2003.
+ """
+
+ def _choose_node(candidates, node_list, delta):
+ if delta > 0:
+ bias_sum = len(node_list) * delta
+ p_delta = bias_sum / (bias_sum + len(candidates))
+ if seed.random() < p_delta:
+ return seed.choice(node_list)
+ return seed.choice(candidates)
+
+ if initial_graph is not None and hasattr(initial_graph, "_adj"):
+ if not isinstance(initial_graph, nx.MultiDiGraph):
+ raise nx.NetworkXError("initial_graph must be a MultiDiGraph.")
+ G = initial_graph
+ else:
+ # Start with 3-cycle
+ G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 0)])
+
+ if alpha <= 0:
+ raise ValueError("alpha must be > 0.")
+ if beta <= 0:
+ raise ValueError("beta must be > 0.")
+ if gamma <= 0:
+ raise ValueError("gamma must be > 0.")
+
+ if abs(alpha + beta + gamma - 1.0) >= 1e-9:
+ raise ValueError("alpha+beta+gamma must equal 1.")
+
+ if delta_in < 0:
+ raise ValueError("delta_in must be >= 0.")
+
+ if delta_out < 0:
+ raise ValueError("delta_out must be >= 0.")
+
+ # pre-populate degree states
+ vs = sum((count * [idx] for idx, count in G.out_degree()), [])
+ ws = sum((count * [idx] for idx, count in G.in_degree()), [])
+
+ # pre-populate node state
+ node_list = list(G.nodes())
+
+ # see if there already are number-based nodes
+ numeric_nodes = [n for n in node_list if isinstance(n, numbers.Number)]
+ if len(numeric_nodes) > 0:
+ # set cursor for new nodes appropriately
+ cursor = max(int(n.real) for n in numeric_nodes) + 1
+ else:
+ # or start at zero
+ cursor = 0
+
+ while len(G) < n:
+ r = seed.random()
+
+ # random choice in alpha,beta,gamma ranges
+ if r < alpha:
+ # alpha
+ # add new node v
+ v = cursor
+ cursor += 1
+ # also add to node state
+ node_list.append(v)
+ # choose w according to in-degree and delta_in
+ w = _choose_node(ws, node_list, delta_in)
+
+ elif r < alpha + beta:
+ # beta
+ # choose v according to out-degree and delta_out
+ v = _choose_node(vs, node_list, delta_out)
+ # choose w according to in-degree and delta_in
+ w = _choose_node(ws, node_list, delta_in)
+
+ else:
+ # gamma
+ # choose v according to out-degree and delta_out
+ v = _choose_node(vs, node_list, delta_out)
+ # add new node w
+ w = cursor
+ cursor += 1
+ # also add to node state
+ node_list.append(w)
+
+ # add edge to graph
+ G.add_edge(v, w)
+
+ # update degree states
+ vs.append(v)
+ ws.append(w)
+
+ return G
+
+
+@py_random_state(4)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_uniform_k_out_graph(n, k, self_loops=True, with_replacement=True, seed=None):
+ """Returns a random `k`-out graph with uniform attachment.
+
+ A random `k`-out graph with uniform attachment is a multidigraph
+ generated by the following algorithm. For each node *u*, choose
+ `k` nodes *v* uniformly at random (with replacement). Add a
+ directed edge joining *u* to *v*.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes in the returned graph.
+
+ k : int
+ The out-degree of each node in the returned graph.
+
+ self_loops : bool
+ If True, self-loops are allowed when generating the graph.
+
+ with_replacement : bool
+ If True, neighbors are chosen with replacement and the
+ returned graph will be a directed multigraph. Otherwise,
+ neighbors are chosen without replacement and the returned graph
+ will be a directed graph.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ NetworkX graph
+ A `k`-out-regular directed graph generated according to the
+ above algorithm. It will be a multigraph if and only if
+ `with_replacement` is True.
+
+ Raises
+ ------
+ ValueError
+ If `with_replacement` is False and `k` is greater than
+ `n`.
+
+ See also
+ --------
+ random_k_out_graph
+
+ Notes
+ -----
+ The return digraph or multidigraph may not be strongly connected, or
+ even weakly connected.
+
+ If `with_replacement` is True, this function is similar to
+ :func:`random_k_out_graph`, if that function had parameter `alpha`
+ set to positive infinity.
+
+ """
+ if with_replacement:
+ create_using = nx.MultiDiGraph()
+
+ def sample(v, nodes):
+ if not self_loops:
+ nodes = nodes - {v}
+ return (seed.choice(list(nodes)) for i in range(k))
+
+ else:
+ create_using = nx.DiGraph()
+
+ def sample(v, nodes):
+ if not self_loops:
+ nodes = nodes - {v}
+ return seed.sample(list(nodes), k)
+
+ G = nx.empty_graph(n, create_using)
+ nodes = set(G)
+ for u in G:
+ G.add_edges_from((u, v) for v in sample(u, nodes))
+ return G
+
+
+@py_random_state(4)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_k_out_graph(n, k, alpha, self_loops=True, seed=None):
+ """Returns a random `k`-out graph with preferential attachment.
+
+ A random `k`-out graph with preferential attachment is a
+ multidigraph generated by the following algorithm.
+
+ 1. Begin with an empty digraph, and initially set each node to have
+ weight `alpha`.
+ 2. Choose a node `u` with out-degree less than `k` uniformly at
+ random.
+ 3. Choose a node `v` from with probability proportional to its
+ weight.
+ 4. Add a directed edge from `u` to `v`, and increase the weight
+ of `v` by one.
+ 5. If each node has out-degree `k`, halt, otherwise repeat from
+ step 2.
+
+ For more information on this model of random graph, see [1].
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes in the returned graph.
+
+ k : int
+ The out-degree of each node in the returned graph.
+
+ alpha : float
+ A positive :class:`float` representing the initial weight of
+ each vertex. A higher number means that in step 3 above, nodes
+ will be chosen more like a true uniformly random sample, and a
+ lower number means that nodes are more likely to be chosen as
+ their in-degree increases. If this parameter is not positive, a
+ :exc:`ValueError` is raised.
+
+ self_loops : bool
+ If True, self-loops are allowed when generating the graph.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ :class:`~networkx.classes.MultiDiGraph`
+ A `k`-out-regular multidigraph generated according to the above
+ algorithm.
+
+ Raises
+ ------
+ ValueError
+ If `alpha` is not positive.
+
+ Notes
+ -----
+ The returned multidigraph may not be strongly connected, or even
+ weakly connected.
+
+ References
+ ----------
+ [1]: Peterson, Nicholas R., and Boris Pittel.
+ "Distance between two random `k`-out digraphs, with and without
+ preferential attachment."
+ arXiv preprint arXiv:1311.5961 (2013).
+ <https://arxiv.org/abs/1311.5961>
+
+ """
+ if alpha < 0:
+ raise ValueError("alpha must be positive")
+ G = nx.empty_graph(n, create_using=nx.MultiDiGraph)
+ weights = Counter({v: alpha for v in G})
+ for i in range(k * n):
+ u = seed.choice([v for v, d in G.out_degree() if d < k])
+ # If self-loops are not allowed, make the source node `u` have
+ # weight zero.
+ if not self_loops:
+ adjustment = Counter({u: weights[u]})
+ else:
+ adjustment = Counter()
+ v = weighted_choice(weights - adjustment, seed=seed)
+ G.add_edge(u, v)
+ weights[v] += 1
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/duplication.py b/.venv/lib/python3.12/site-packages/networkx/generators/duplication.py
new file mode 100644
index 00000000..3c3ade63
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/duplication.py
@@ -0,0 +1,174 @@
+"""Functions for generating graphs based on the "duplication" method.
+
+These graph generators start with a small initial graph then duplicate
+nodes and (partially) duplicate their edges. These functions are
+generally inspired by biological networks.
+
+"""
+
+import networkx as nx
+from networkx.exception import NetworkXError
+from networkx.utils import py_random_state
+from networkx.utils.misc import check_create_using
+
+__all__ = ["partial_duplication_graph", "duplication_divergence_graph"]
+
+
+@py_random_state(4)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def partial_duplication_graph(N, n, p, q, seed=None, *, create_using=None):
+ """Returns a random graph using the partial duplication model.
+
+ Parameters
+ ----------
+ N : int
+ The total number of nodes in the final graph.
+
+ n : int
+ The number of nodes in the initial clique.
+
+ p : float
+ The probability of joining each neighbor of a node to the
+ duplicate node. Must be a number in the between zero and one,
+ inclusive.
+
+ q : float
+ The probability of joining the source node to the duplicate
+ node. Must be a number in the between zero and one, inclusive.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Notes
+ -----
+ A graph of nodes is grown by creating a fully connected graph
+ of size `n`. The following procedure is then repeated until
+ a total of `N` nodes have been reached.
+
+ 1. A random node, *u*, is picked and a new node, *v*, is created.
+ 2. For each neighbor of *u* an edge from the neighbor to *v* is created
+ with probability `p`.
+ 3. An edge from *u* to *v* is created with probability `q`.
+
+ This algorithm appears in [1].
+
+ This implementation allows the possibility of generating
+ disconnected graphs.
+
+ References
+ ----------
+ .. [1] Knudsen Michael, and Carsten Wiuf. "A Markov chain approach to
+ randomly grown graphs." Journal of Applied Mathematics 2008.
+ <https://doi.org/10.1155/2008/190836>
+
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ if p < 0 or p > 1 or q < 0 or q > 1:
+ msg = "partial duplication graph must have 0 <= p, q <= 1."
+ raise NetworkXError(msg)
+ if n > N:
+ raise NetworkXError("partial duplication graph must have n <= N.")
+
+ G = nx.complete_graph(n, create_using)
+ for new_node in range(n, N):
+ # Pick a random vertex, u, already in the graph.
+ src_node = seed.randint(0, new_node - 1)
+
+ # Add a new vertex, v, to the graph.
+ G.add_node(new_node)
+
+ # For each neighbor of u...
+ for nbr_node in list(nx.all_neighbors(G, src_node)):
+ # Add the neighbor to v with probability p.
+ if seed.random() < p:
+ G.add_edge(new_node, nbr_node)
+
+ # Join v and u with probability q.
+ if seed.random() < q:
+ G.add_edge(new_node, src_node)
+ return G
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def duplication_divergence_graph(n, p, seed=None, *, create_using=None):
+ """Returns an undirected graph using the duplication-divergence model.
+
+ A graph of `n` nodes is created by duplicating the initial nodes
+ and retaining edges incident to the original nodes with a retention
+ probability `p`.
+
+ Parameters
+ ----------
+ n : int
+ The desired number of nodes in the graph.
+ p : float
+ The probability for retaining the edge of the replicated node.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Returns
+ -------
+ G : Graph
+
+ Raises
+ ------
+ NetworkXError
+ If `p` is not a valid probability.
+ If `n` is less than 2.
+
+ Notes
+ -----
+ This algorithm appears in [1].
+
+ This implementation disallows the possibility of generating
+ disconnected graphs.
+
+ References
+ ----------
+ .. [1] I. Ispolatov, P. L. Krapivsky, A. Yuryev,
+ "Duplication-divergence model of protein interaction network",
+ Phys. Rev. E, 71, 061911, 2005.
+
+ """
+ if p > 1 or p < 0:
+ msg = f"NetworkXError p={p} is not in [0,1]."
+ raise nx.NetworkXError(msg)
+ if n < 2:
+ msg = "n must be greater than or equal to 2"
+ raise nx.NetworkXError(msg)
+
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ G = nx.empty_graph(create_using=create_using)
+
+ # Initialize the graph with two connected nodes.
+ G.add_edge(0, 1)
+ i = 2
+ while i < n:
+ # Choose a random node from current graph to duplicate.
+ random_node = seed.choice(list(G))
+ # Make the replica.
+ G.add_node(i)
+ # flag indicates whether at least one edge is connected on the replica.
+ flag = False
+ for nbr in G.neighbors(random_node):
+ if seed.random() < p:
+ # Link retention step.
+ G.add_edge(i, nbr)
+ flag = True
+ if not flag:
+ # Delete replica if no edges retained.
+ G.remove_node(i)
+ else:
+ # Successful duplication.
+ i += 1
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/ego.py b/.venv/lib/python3.12/site-packages/networkx/generators/ego.py
new file mode 100644
index 00000000..1c705430
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/ego.py
@@ -0,0 +1,66 @@
+"""
+Ego graph.
+"""
+
+__all__ = ["ego_graph"]
+
+import networkx as nx
+
+
+@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
+def ego_graph(G, n, radius=1, center=True, undirected=False, distance=None):
+ """Returns induced subgraph of neighbors centered at node n within
+ a given radius.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX Graph or DiGraph
+
+ n : node
+ A single node
+
+ radius : number, optional
+ Include all neighbors of distance<=radius from n.
+
+ center : bool, optional
+ If False, do not include center node in graph
+
+ undirected : bool, optional
+ If True use both in- and out-neighbors of directed graphs.
+
+ distance : key, optional
+ Use specified edge data key as distance. For example, setting
+ distance='weight' will use the edge weight to measure the
+ distance from the node n.
+
+ Notes
+ -----
+ For directed graphs D this produces the "out" neighborhood
+ or successors. If you want the neighborhood of predecessors
+ first reverse the graph with D.reverse(). If you want both
+ directions use the keyword argument undirected=True.
+
+ Node, edge, and graph attributes are copied to the returned subgraph.
+ """
+ if undirected:
+ if distance is not None:
+ sp, _ = nx.single_source_dijkstra(
+ G.to_undirected(), n, cutoff=radius, weight=distance
+ )
+ else:
+ sp = dict(
+ nx.single_source_shortest_path_length(
+ G.to_undirected(), n, cutoff=radius
+ )
+ )
+ else:
+ if distance is not None:
+ sp, _ = nx.single_source_dijkstra(G, n, cutoff=radius, weight=distance)
+ else:
+ sp = dict(nx.single_source_shortest_path_length(G, n, cutoff=radius))
+
+ H = G.subgraph(sp).copy()
+ if not center:
+ H.remove_node(n)
+ return H
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/expanders.py b/.venv/lib/python3.12/site-packages/networkx/generators/expanders.py
new file mode 100644
index 00000000..befdb0e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/expanders.py
@@ -0,0 +1,474 @@
+"""Provides explicit constructions of expander graphs."""
+
+import itertools
+
+import networkx as nx
+
+__all__ = [
+ "margulis_gabber_galil_graph",
+ "chordal_cycle_graph",
+ "paley_graph",
+ "maybe_regular_expander",
+ "is_regular_expander",
+ "random_regular_expander_graph",
+]
+
+
+# Other discrete torus expanders can be constructed by using the following edge
+# sets. For more information, see Chapter 4, "Expander Graphs", in
+# "Pseudorandomness", by Salil Vadhan.
+#
+# For a directed expander, add edges from (x, y) to:
+#
+# (x, y),
+# ((x + 1) % n, y),
+# (x, (y + 1) % n),
+# (x, (x + y) % n),
+# (-y % n, x)
+#
+# For an undirected expander, add the reverse edges.
+#
+# Also appearing in the paper of Gabber and Galil:
+#
+# (x, y),
+# (x, (x + y) % n),
+# (x, (x + y + 1) % n),
+# ((x + y) % n, y),
+# ((x + y + 1) % n, y)
+#
+# and:
+#
+# (x, y),
+# ((x + 2*y) % n, y),
+# ((x + (2*y + 1)) % n, y),
+# ((x + (2*y + 2)) % n, y),
+# (x, (y + 2*x) % n),
+# (x, (y + (2*x + 1)) % n),
+# (x, (y + (2*x + 2)) % n),
+#
+@nx._dispatchable(graphs=None, returns_graph=True)
+def margulis_gabber_galil_graph(n, create_using=None):
+ r"""Returns the Margulis-Gabber-Galil undirected MultiGraph on `n^2` nodes.
+
+ The undirected MultiGraph is regular with degree `8`. Nodes are integer
+ pairs. The second-largest eigenvalue of the adjacency matrix of the graph
+ is at most `5 \sqrt{2}`, regardless of `n`.
+
+ Parameters
+ ----------
+ n : int
+ Determines the number of nodes in the graph: `n^2`.
+ create_using : NetworkX graph constructor, optional (default MultiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : graph
+ The constructed undirected multigraph.
+
+ Raises
+ ------
+ NetworkXError
+ If the graph is directed or not a multigraph.
+
+ """
+ G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
+ if G.is_directed() or not G.is_multigraph():
+ msg = "`create_using` must be an undirected multigraph."
+ raise nx.NetworkXError(msg)
+
+ for x, y in itertools.product(range(n), repeat=2):
+ for u, v in (
+ ((x + 2 * y) % n, y),
+ ((x + (2 * y + 1)) % n, y),
+ (x, (y + 2 * x) % n),
+ (x, (y + (2 * x + 1)) % n),
+ ):
+ G.add_edge((x, y), (u, v))
+ G.graph["name"] = f"margulis_gabber_galil_graph({n})"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def chordal_cycle_graph(p, create_using=None):
+ """Returns the chordal cycle graph on `p` nodes.
+
+ The returned graph is a cycle graph on `p` nodes with chords joining each
+ vertex `x` to its inverse modulo `p`. This graph is a (mildly explicit)
+ 3-regular expander [1]_.
+
+ `p` *must* be a prime number.
+
+ Parameters
+ ----------
+ p : a prime number
+
+ The number of vertices in the graph. This also indicates where the
+ chordal edges in the cycle will be created.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : graph
+ The constructed undirected multigraph.
+
+ Raises
+ ------
+ NetworkXError
+
+ If `create_using` indicates directed or not a multigraph.
+
+ References
+ ----------
+
+ .. [1] Theorem 4.4.2 in A. Lubotzky. "Discrete groups, expanding graphs and
+ invariant measures", volume 125 of Progress in Mathematics.
+ Birkhäuser Verlag, Basel, 1994.
+
+ """
+ G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
+ if G.is_directed() or not G.is_multigraph():
+ msg = "`create_using` must be an undirected multigraph."
+ raise nx.NetworkXError(msg)
+
+ for x in range(p):
+ left = (x - 1) % p
+ right = (x + 1) % p
+ # Here we apply Fermat's Little Theorem to compute the multiplicative
+ # inverse of x in Z/pZ. By Fermat's Little Theorem,
+ #
+ # x^p = x (mod p)
+ #
+ # Therefore,
+ #
+ # x * x^(p - 2) = 1 (mod p)
+ #
+ # The number 0 is a special case: we just let its inverse be itself.
+ chord = pow(x, p - 2, p) if x > 0 else 0
+ for y in (left, right, chord):
+ G.add_edge(x, y)
+ G.graph["name"] = f"chordal_cycle_graph({p})"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def paley_graph(p, create_using=None):
+ r"""Returns the Paley $\frac{(p-1)}{2}$ -regular graph on $p$ nodes.
+
+ The returned graph is a graph on $\mathbb{Z}/p\mathbb{Z}$ with edges between $x$ and $y$
+ if and only if $x-y$ is a nonzero square in $\mathbb{Z}/p\mathbb{Z}$.
+
+ If $p \equiv 1 \pmod 4$, $-1$ is a square in $\mathbb{Z}/p\mathbb{Z}$ and therefore $x-y$ is a square if and
+ only if $y-x$ is also a square, i.e the edges in the Paley graph are symmetric.
+
+ If $p \equiv 3 \pmod 4$, $-1$ is not a square in $\mathbb{Z}/p\mathbb{Z}$ and therefore either $x-y$ or $y-x$
+ is a square in $\mathbb{Z}/p\mathbb{Z}$ but not both.
+
+ Note that a more general definition of Paley graphs extends this construction
+ to graphs over $q=p^n$ vertices, by using the finite field $F_q$ instead of $\mathbb{Z}/p\mathbb{Z}$.
+ This construction requires to compute squares in general finite fields and is
+ not what is implemented here (i.e `paley_graph(25)` does not return the true
+ Paley graph associated with $5^2$).
+
+ Parameters
+ ----------
+ p : int, an odd prime number.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : graph
+ The constructed directed graph.
+
+ Raises
+ ------
+ NetworkXError
+ If the graph is a multigraph.
+
+ References
+ ----------
+ Chapter 13 in B. Bollobas, Random Graphs. Second edition.
+ Cambridge Studies in Advanced Mathematics, 73.
+ Cambridge University Press, Cambridge (2001).
+ """
+ G = nx.empty_graph(0, create_using, default=nx.DiGraph)
+ if G.is_multigraph():
+ msg = "`create_using` cannot be a multigraph."
+ raise nx.NetworkXError(msg)
+
+ # Compute the squares in Z/pZ.
+ # Make it a set to uniquify (there are exactly (p-1)/2 squares in Z/pZ
+ # when is prime).
+ square_set = {(x**2) % p for x in range(1, p) if (x**2) % p != 0}
+
+ for x in range(p):
+ for x2 in square_set:
+ G.add_edge(x, (x + x2) % p)
+ G.graph["name"] = f"paley({p})"
+ return G
+
+
+@nx.utils.decorators.np_random_state("seed")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def maybe_regular_expander(n, d, *, create_using=None, max_tries=100, seed=None):
+ r"""Utility for creating a random regular expander.
+
+ Returns a random $d$-regular graph on $n$ nodes which is an expander
+ graph with very good probability.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ d : int
+ The degree of each node.
+ create_using : Graph Instance or Constructor
+ Indicator of type of graph to return.
+ If a Graph-type instance, then clear and use it.
+ If a constructor, call it to create an empty graph.
+ Use the Graph constructor by default.
+ max_tries : int. (default: 100)
+ The number of allowed loops when generating each independent cycle
+ seed : (default: None)
+ Seed used to set random number generation state. See :ref`Randomness<randomness>`.
+
+ Notes
+ -----
+ The nodes are numbered from $0$ to $n - 1$.
+
+ The graph is generated by taking $d / 2$ random independent cycles.
+
+ Joel Friedman proved that in this model the resulting
+ graph is an expander with probability
+ $1 - O(n^{-\tau})$ where $\tau = \lceil (\sqrt{d - 1}) / 2 \rceil - 1$. [1]_
+
+ Examples
+ --------
+ >>> G = nx.maybe_regular_expander(n=200, d=6, seed=8020)
+
+ Returns
+ -------
+ G : graph
+ The constructed undirected graph.
+
+ Raises
+ ------
+ NetworkXError
+ If $d % 2 != 0$ as the degree must be even.
+ If $n - 1$ is less than $ 2d $ as the graph is complete at most.
+ If max_tries is reached
+
+ See Also
+ --------
+ is_regular_expander
+ random_regular_expander_graph
+
+ References
+ ----------
+ .. [1] Joel Friedman,
+ A Proof of Alon’s Second Eigenvalue Conjecture and Related Problems, 2004
+ https://arxiv.org/abs/cs/0405020
+
+ """
+
+ import numpy as np
+
+ if n < 1:
+ raise nx.NetworkXError("n must be a positive integer")
+
+ if not (d >= 2):
+ raise nx.NetworkXError("d must be greater than or equal to 2")
+
+ if not (d % 2 == 0):
+ raise nx.NetworkXError("d must be even")
+
+ if not (n - 1 >= d):
+ raise nx.NetworkXError(
+ f"Need n-1>= d to have room for {d//2} independent cycles with {n} nodes"
+ )
+
+ G = nx.empty_graph(n, create_using)
+
+ if n < 2:
+ return G
+
+ cycles = []
+ edges = set()
+
+ # Create d / 2 cycles
+ for i in range(d // 2):
+ iterations = max_tries
+ # Make sure the cycles are independent to have a regular graph
+ while len(edges) != (i + 1) * n:
+ iterations -= 1
+ # Faster than random.permutation(n) since there are only
+ # (n-1)! distinct cycles against n! permutations of size n
+ cycle = seed.permutation(n - 1).tolist()
+ cycle.append(n - 1)
+
+ new_edges = {
+ (u, v)
+ for u, v in nx.utils.pairwise(cycle, cyclic=True)
+ if (u, v) not in edges and (v, u) not in edges
+ }
+ # If the new cycle has no edges in common with previous cycles
+ # then add it to the list otherwise try again
+ if len(new_edges) == n:
+ cycles.append(cycle)
+ edges.update(new_edges)
+
+ if iterations == 0:
+ raise nx.NetworkXError("Too many iterations in maybe_regular_expander")
+
+ G.add_edges_from(edges)
+
+ return G
+
+
+@nx.utils.not_implemented_for("directed")
+@nx.utils.not_implemented_for("multigraph")
+@nx._dispatchable(preserve_edge_attrs={"G": {"weight": 1}})
+def is_regular_expander(G, *, epsilon=0):
+ r"""Determines whether the graph G is a regular expander. [1]_
+
+ An expander graph is a sparse graph with strong connectivity properties.
+
+ More precisely, this helper checks whether the graph is a
+ regular $(n, d, \lambda)$-expander with $\lambda$ close to
+ the Alon-Boppana bound and given by
+ $\lambda = 2 \sqrt{d - 1} + \epsilon$. [2]_
+
+ In the case where $\epsilon = 0$ then if the graph successfully passes the test
+ it is a Ramanujan graph. [3]_
+
+ A Ramanujan graph has spectral gap almost as large as possible, which makes them
+ excellent expanders.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ epsilon : int, float, default=0
+
+ Returns
+ -------
+ bool
+ Whether the given graph is a regular $(n, d, \lambda)$-expander
+ where $\lambda = 2 \sqrt{d - 1} + \epsilon$.
+
+ Examples
+ --------
+ >>> G = nx.random_regular_expander_graph(20, 4)
+ >>> nx.is_regular_expander(G)
+ True
+
+ See Also
+ --------
+ maybe_regular_expander
+ random_regular_expander_graph
+
+ References
+ ----------
+ .. [1] Expander graph, https://en.wikipedia.org/wiki/Expander_graph
+ .. [2] Alon-Boppana bound, https://en.wikipedia.org/wiki/Alon%E2%80%93Boppana_bound
+ .. [3] Ramanujan graphs, https://en.wikipedia.org/wiki/Ramanujan_graph
+
+ """
+
+ import numpy as np
+ from scipy.sparse.linalg import eigsh
+
+ if epsilon < 0:
+ raise nx.NetworkXError("epsilon must be non negative")
+
+ if not nx.is_regular(G):
+ return False
+
+ _, d = nx.utils.arbitrary_element(G.degree)
+
+ A = nx.adjacency_matrix(G, dtype=float)
+ lams = eigsh(A, which="LM", k=2, return_eigenvectors=False)
+
+ # lambda2 is the second biggest eigenvalue
+ lambda2 = min(lams)
+
+ # Use bool() to convert numpy scalar to Python Boolean
+ return bool(abs(lambda2) < 2 ** np.sqrt(d - 1) + epsilon)
+
+
+@nx.utils.decorators.np_random_state("seed")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_regular_expander_graph(
+ n, d, *, epsilon=0, create_using=None, max_tries=100, seed=None
+):
+ r"""Returns a random regular expander graph on $n$ nodes with degree $d$.
+
+ An expander graph is a sparse graph with strong connectivity properties. [1]_
+
+ More precisely the returned graph is a $(n, d, \lambda)$-expander with
+ $\lambda = 2 \sqrt{d - 1} + \epsilon$, close to the Alon-Boppana bound. [2]_
+
+ In the case where $\epsilon = 0$ it returns a Ramanujan graph.
+ A Ramanujan graph has spectral gap almost as large as possible,
+ which makes them excellent expanders. [3]_
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ d : int
+ The degree of each node.
+ epsilon : int, float, default=0
+ max_tries : int, (default: 100)
+ The number of allowed loops, also used in the maybe_regular_expander utility
+ seed : (default: None)
+ Seed used to set random number generation state. See :ref`Randomness<randomness>`.
+
+ Raises
+ ------
+ NetworkXError
+ If max_tries is reached
+
+ Examples
+ --------
+ >>> G = nx.random_regular_expander_graph(20, 4)
+ >>> nx.is_regular_expander(G)
+ True
+
+ Notes
+ -----
+ This loops over `maybe_regular_expander` and can be slow when
+ $n$ is too big or $\epsilon$ too small.
+
+ See Also
+ --------
+ maybe_regular_expander
+ is_regular_expander
+
+ References
+ ----------
+ .. [1] Expander graph, https://en.wikipedia.org/wiki/Expander_graph
+ .. [2] Alon-Boppana bound, https://en.wikipedia.org/wiki/Alon%E2%80%93Boppana_bound
+ .. [3] Ramanujan graphs, https://en.wikipedia.org/wiki/Ramanujan_graph
+
+ """
+ G = maybe_regular_expander(
+ n, d, create_using=create_using, max_tries=max_tries, seed=seed
+ )
+ iterations = max_tries
+
+ while not is_regular_expander(G, epsilon=epsilon):
+ iterations -= 1
+ G = maybe_regular_expander(
+ n=n, d=d, create_using=create_using, max_tries=max_tries, seed=seed
+ )
+
+ if iterations == 0:
+ raise nx.NetworkXError(
+ "Too many iterations in random_regular_expander_graph"
+ )
+
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/geometric.py b/.venv/lib/python3.12/site-packages/networkx/generators/geometric.py
new file mode 100644
index 00000000..7f19281b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/geometric.py
@@ -0,0 +1,1048 @@
+"""Generators for geometric graphs."""
+
+import math
+from bisect import bisect_left
+from itertools import accumulate, combinations, product
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+__all__ = [
+ "geometric_edges",
+ "geographical_threshold_graph",
+ "navigable_small_world_graph",
+ "random_geometric_graph",
+ "soft_random_geometric_graph",
+ "thresholded_random_geometric_graph",
+ "waxman_graph",
+ "geometric_soft_configuration_graph",
+]
+
+
+@nx._dispatchable(node_attrs="pos_name")
+def geometric_edges(G, radius, p=2, *, pos_name="pos"):
+ """Returns edge list of node pairs within `radius` of each other.
+
+ Parameters
+ ----------
+ G : networkx graph
+ The graph from which to generate the edge list. The nodes in `G` should
+ have an attribute ``pos`` corresponding to the node position, which is
+ used to compute the distance to other nodes.
+ radius : scalar
+ The distance threshold. Edges are included in the edge list if the
+ distance between the two nodes is less than `radius`.
+ pos_name : string, default="pos"
+ The name of the node attribute which represents the position of each
+ node in 2D coordinates. Every node in the Graph must have this attribute.
+ p : scalar, default=2
+ The `Minkowski distance metric
+ <https://en.wikipedia.org/wiki/Minkowski_distance>`_ used to compute
+ distances. The default value is 2, i.e. Euclidean distance.
+
+ Returns
+ -------
+ edges : list
+ List of edges whose distances are less than `radius`
+
+ Notes
+ -----
+ Radius uses Minkowski distance metric `p`.
+ If scipy is available, `scipy.spatial.cKDTree` is used to speed computation.
+
+ Examples
+ --------
+ Create a graph with nodes that have a "pos" attribute representing 2D
+ coordinates.
+
+ >>> G = nx.Graph()
+ >>> G.add_nodes_from(
+ ... [
+ ... (0, {"pos": (0, 0)}),
+ ... (1, {"pos": (3, 0)}),
+ ... (2, {"pos": (8, 0)}),
+ ... ]
+ ... )
+ >>> nx.geometric_edges(G, radius=1)
+ []
+ >>> nx.geometric_edges(G, radius=4)
+ [(0, 1)]
+ >>> nx.geometric_edges(G, radius=6)
+ [(0, 1), (1, 2)]
+ >>> nx.geometric_edges(G, radius=9)
+ [(0, 1), (0, 2), (1, 2)]
+ """
+ # Input validation - every node must have a "pos" attribute
+ for n, pos in G.nodes(data=pos_name):
+ if pos is None:
+ raise nx.NetworkXError(
+ f"Node {n} (and all nodes) must have a '{pos_name}' attribute."
+ )
+
+ # NOTE: See _geometric_edges for the actual implementation. The reason this
+ # is split into two functions is to avoid the overhead of input validation
+ # every time the function is called internally in one of the other
+ # geometric generators
+ return _geometric_edges(G, radius, p, pos_name)
+
+
+def _geometric_edges(G, radius, p, pos_name):
+ """
+ Implements `geometric_edges` without input validation. See `geometric_edges`
+ for complete docstring.
+ """
+ nodes_pos = G.nodes(data=pos_name)
+ try:
+ import scipy as sp
+ except ImportError:
+ # no scipy KDTree so compute by for-loop
+ radius_p = radius**p
+ edges = [
+ (u, v)
+ for (u, pu), (v, pv) in combinations(nodes_pos, 2)
+ if sum(abs(a - b) ** p for a, b in zip(pu, pv)) <= radius_p
+ ]
+ return edges
+ # scipy KDTree is available
+ nodes, coords = list(zip(*nodes_pos))
+ kdtree = sp.spatial.cKDTree(coords) # Cannot provide generator.
+ edge_indexes = kdtree.query_pairs(radius, p)
+ edges = [(nodes[u], nodes[v]) for u, v in sorted(edge_indexes)]
+ return edges
+
+
+@py_random_state(5)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_geometric_graph(
+ n, radius, dim=2, pos=None, p=2, seed=None, *, pos_name="pos"
+):
+ """Returns a random geometric graph in the unit cube of dimensions `dim`.
+
+ The random geometric graph model places `n` nodes uniformly at
+ random in the unit cube. Two nodes are joined by an edge if the
+ distance between the nodes is at most `radius`.
+
+ Edges are determined using a KDTree when SciPy is available.
+ This reduces the time complexity from $O(n^2)$ to $O(n)$.
+
+ Parameters
+ ----------
+ n : int or iterable
+ Number of nodes or iterable of nodes
+ radius: float
+ Distance threshold value
+ dim : int, optional
+ Dimension of graph
+ pos : dict, optional
+ A dictionary keyed by node with node positions as values.
+ p : float, optional
+ Which Minkowski distance metric to use. `p` has to meet the condition
+ ``1 <= p <= infinity``.
+
+ If this argument is not specified, the :math:`L^2` metric
+ (the Euclidean distance metric), p = 2 is used.
+ This should not be confused with the `p` of an Erdős-Rényi random
+ graph, which represents probability.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ pos_name : string, default="pos"
+ The name of the node attribute which represents the position
+ in 2D coordinates of the node in the returned graph.
+
+ Returns
+ -------
+ Graph
+ A random geometric graph, undirected and without self-loops.
+ Each node has a node attribute ``'pos'`` that stores the
+ position of that node in Euclidean space as provided by the
+ ``pos`` keyword argument or, if ``pos`` was not provided, as
+ generated by this function.
+
+ Examples
+ --------
+ Create a random geometric graph on twenty nodes where nodes are joined by
+ an edge if their distance is at most 0.1::
+
+ >>> G = nx.random_geometric_graph(20, 0.1)
+
+ Notes
+ -----
+ This uses a *k*-d tree to build the graph.
+
+ The `pos` keyword argument can be used to specify node positions so you
+ can create an arbitrary distribution and domain for positions.
+
+ For example, to use a 2D Gaussian distribution of node positions with mean
+ (0, 0) and standard deviation 2::
+
+ >>> import random
+ >>> n = 20
+ >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)}
+ >>> G = nx.random_geometric_graph(n, 0.2, pos=pos)
+
+ References
+ ----------
+ .. [1] Penrose, Mathew, *Random Geometric Graphs*,
+ Oxford Studies in Probability, 5, 2003.
+
+ """
+ # TODO Is this function just a special case of the geographical
+ # threshold graph?
+ #
+ # half_radius = {v: radius / 2 for v in n}
+ # return geographical_threshold_graph(nodes, theta=1, alpha=1,
+ # weight=half_radius)
+ #
+ G = nx.empty_graph(n)
+ # If no positions are provided, choose uniformly random vectors in
+ # Euclidean space of the specified dimension.
+ if pos is None:
+ pos = {v: [seed.random() for i in range(dim)] for v in G}
+ nx.set_node_attributes(G, pos, pos_name)
+
+ G.add_edges_from(_geometric_edges(G, radius, p, pos_name))
+ return G
+
+
+@py_random_state(6)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def soft_random_geometric_graph(
+ n, radius, dim=2, pos=None, p=2, p_dist=None, seed=None, *, pos_name="pos"
+):
+ r"""Returns a soft random geometric graph in the unit cube.
+
+ The soft random geometric graph [1] model places `n` nodes uniformly at
+ random in the unit cube in dimension `dim`. Two nodes of distance, `dist`,
+ computed by the `p`-Minkowski distance metric are joined by an edge with
+ probability `p_dist` if the computed distance metric value of the nodes
+ is at most `radius`, otherwise they are not joined.
+
+ Edges within `radius` of each other are determined using a KDTree when
+ SciPy is available. This reduces the time complexity from :math:`O(n^2)`
+ to :math:`O(n)`.
+
+ Parameters
+ ----------
+ n : int or iterable
+ Number of nodes or iterable of nodes
+ radius: float
+ Distance threshold value
+ dim : int, optional
+ Dimension of graph
+ pos : dict, optional
+ A dictionary keyed by node with node positions as values.
+ p : float, optional
+ Which Minkowski distance metric to use.
+ `p` has to meet the condition ``1 <= p <= infinity``.
+
+ If this argument is not specified, the :math:`L^2` metric
+ (the Euclidean distance metric), p = 2 is used.
+
+ This should not be confused with the `p` of an Erdős-Rényi random
+ graph, which represents probability.
+ p_dist : function, optional
+ A probability density function computing the probability of
+ connecting two nodes that are of distance, dist, computed by the
+ Minkowski distance metric. The probability density function, `p_dist`,
+ must be any function that takes the metric value as input
+ and outputs a single probability value between 0-1. The scipy.stats
+ package has many probability distribution functions implemented and
+ tools for custom probability distribution definitions [2], and passing
+ the .pdf method of scipy.stats distributions can be used here. If the
+ probability function, `p_dist`, is not supplied, the default function
+ is an exponential distribution with rate parameter :math:`\lambda=1`.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ pos_name : string, default="pos"
+ The name of the node attribute which represents the position
+ in 2D coordinates of the node in the returned graph.
+
+ Returns
+ -------
+ Graph
+ A soft random geometric graph, undirected and without self-loops.
+ Each node has a node attribute ``'pos'`` that stores the
+ position of that node in Euclidean space as provided by the
+ ``pos`` keyword argument or, if ``pos`` was not provided, as
+ generated by this function.
+
+ Examples
+ --------
+ Default Graph:
+
+ G = nx.soft_random_geometric_graph(50, 0.2)
+
+ Custom Graph:
+
+ Create a soft random geometric graph on 100 uniformly distributed nodes
+ where nodes are joined by an edge with probability computed from an
+ exponential distribution with rate parameter :math:`\lambda=1` if their
+ Euclidean distance is at most 0.2.
+
+ Notes
+ -----
+ This uses a *k*-d tree to build the graph.
+
+ The `pos` keyword argument can be used to specify node positions so you
+ can create an arbitrary distribution and domain for positions.
+
+ For example, to use a 2D Gaussian distribution of node positions with mean
+ (0, 0) and standard deviation 2
+
+ The scipy.stats package can be used to define the probability distribution
+ with the .pdf method used as `p_dist`.
+
+ ::
+
+ >>> import random
+ >>> import math
+ >>> n = 100
+ >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)}
+ >>> p_dist = lambda dist: math.exp(-dist)
+ >>> G = nx.soft_random_geometric_graph(n, 0.2, pos=pos, p_dist=p_dist)
+
+ References
+ ----------
+ .. [1] Penrose, Mathew D. "Connectivity of soft random geometric graphs."
+ The Annals of Applied Probability 26.2 (2016): 986-1028.
+ .. [2] scipy.stats -
+ https://docs.scipy.org/doc/scipy/reference/tutorial/stats.html
+
+ """
+ G = nx.empty_graph(n)
+ G.name = f"soft_random_geometric_graph({n}, {radius}, {dim})"
+ # If no positions are provided, choose uniformly random vectors in
+ # Euclidean space of the specified dimension.
+ if pos is None:
+ pos = {v: [seed.random() for i in range(dim)] for v in G}
+ nx.set_node_attributes(G, pos, pos_name)
+
+ # if p_dist function not supplied the default function is an exponential
+ # distribution with rate parameter :math:`\lambda=1`.
+ if p_dist is None:
+
+ def p_dist(dist):
+ return math.exp(-dist)
+
+ def should_join(edge):
+ u, v = edge
+ dist = (sum(abs(a - b) ** p for a, b in zip(pos[u], pos[v]))) ** (1 / p)
+ return seed.random() < p_dist(dist)
+
+ G.add_edges_from(filter(should_join, _geometric_edges(G, radius, p, pos_name)))
+ return G
+
+
+@py_random_state(7)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def geographical_threshold_graph(
+ n,
+ theta,
+ dim=2,
+ pos=None,
+ weight=None,
+ metric=None,
+ p_dist=None,
+ seed=None,
+ *,
+ pos_name="pos",
+ weight_name="weight",
+):
+ r"""Returns a geographical threshold graph.
+
+ The geographical threshold graph model places $n$ nodes uniformly at
+ random in a rectangular domain. Each node $u$ is assigned a weight
+ $w_u$. Two nodes $u$ and $v$ are joined by an edge if
+
+ .. math::
+
+ (w_u + w_v)p_{dist}(r) \ge \theta
+
+ where `r` is the distance between `u` and `v`, `p_dist` is any function of
+ `r`, and :math:`\theta` as the threshold parameter. `p_dist` is used to
+ give weight to the distance between nodes when deciding whether or not
+ they should be connected. The larger `p_dist` is, the more prone nodes
+ separated by `r` are to be connected, and vice versa.
+
+ Parameters
+ ----------
+ n : int or iterable
+ Number of nodes or iterable of nodes
+ theta: float
+ Threshold value
+ dim : int, optional
+ Dimension of graph
+ pos : dict
+ Node positions as a dictionary of tuples keyed by node.
+ weight : dict
+ Node weights as a dictionary of numbers keyed by node.
+ metric : function
+ A metric on vectors of numbers (represented as lists or
+ tuples). This must be a function that accepts two lists (or
+ tuples) as input and yields a number as output. The function
+ must also satisfy the four requirements of a `metric`_.
+ Specifically, if $d$ is the function and $x$, $y$,
+ and $z$ are vectors in the graph, then $d$ must satisfy
+
+ 1. $d(x, y) \ge 0$,
+ 2. $d(x, y) = 0$ if and only if $x = y$,
+ 3. $d(x, y) = d(y, x)$,
+ 4. $d(x, z) \le d(x, y) + d(y, z)$.
+
+ If this argument is not specified, the Euclidean distance metric is
+ used.
+
+ .. _metric: https://en.wikipedia.org/wiki/Metric_%28mathematics%29
+ p_dist : function, optional
+ Any function used to give weight to the distance between nodes when
+ deciding whether or not they should be connected. `p_dist` was
+ originally conceived as a probability density function giving the
+ probability of connecting two nodes that are of metric distance `r`
+ apart. The implementation here allows for more arbitrary definitions
+ of `p_dist` that do not need to correspond to valid probability
+ density functions. The :mod:`scipy.stats` package has many
+ probability density functions implemented and tools for custom
+ probability density definitions, and passing the ``.pdf`` method of
+ scipy.stats distributions can be used here. If ``p_dist=None``
+ (the default), the exponential function :math:`r^{-2}` is used.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ pos_name : string, default="pos"
+ The name of the node attribute which represents the position
+ in 2D coordinates of the node in the returned graph.
+ weight_name : string, default="weight"
+ The name of the node attribute which represents the weight
+ of the node in the returned graph.
+
+ Returns
+ -------
+ Graph
+ A random geographic threshold graph, undirected and without
+ self-loops.
+
+ Each node has a node attribute ``pos`` that stores the
+ position of that node in Euclidean space as provided by the
+ ``pos`` keyword argument or, if ``pos`` was not provided, as
+ generated by this function. Similarly, each node has a node
+ attribute ``weight`` that stores the weight of that node as
+ provided or as generated.
+
+ Examples
+ --------
+ Specify an alternate distance metric using the ``metric`` keyword
+ argument. For example, to use the `taxicab metric`_ instead of the
+ default `Euclidean metric`_::
+
+ >>> dist = lambda x, y: sum(abs(a - b) for a, b in zip(x, y))
+ >>> G = nx.geographical_threshold_graph(10, 0.1, metric=dist)
+
+ .. _taxicab metric: https://en.wikipedia.org/wiki/Taxicab_geometry
+ .. _Euclidean metric: https://en.wikipedia.org/wiki/Euclidean_distance
+
+ Notes
+ -----
+ If weights are not specified they are assigned to nodes by drawing randomly
+ from the exponential distribution with rate parameter $\lambda=1$.
+ To specify weights from a different distribution, use the `weight` keyword
+ argument::
+
+ >>> import random
+ >>> n = 20
+ >>> w = {i: random.expovariate(5.0) for i in range(n)}
+ >>> G = nx.geographical_threshold_graph(20, 50, weight=w)
+
+ If node positions are not specified they are randomly assigned from the
+ uniform distribution.
+
+ References
+ ----------
+ .. [1] Masuda, N., Miwa, H., Konno, N.:
+ Geographical threshold graphs with small-world and scale-free
+ properties.
+ Physical Review E 71, 036108 (2005)
+ .. [2] Milan Bradonjić, Aric Hagberg and Allon G. Percus,
+ Giant component and connectivity in geographical threshold graphs,
+ in Algorithms and Models for the Web-Graph (WAW 2007),
+ Antony Bonato and Fan Chung (Eds), pp. 209--216, 2007
+ """
+ G = nx.empty_graph(n)
+ # If no weights are provided, choose them from an exponential
+ # distribution.
+ if weight is None:
+ weight = {v: seed.expovariate(1) for v in G}
+ # If no positions are provided, choose uniformly random vectors in
+ # Euclidean space of the specified dimension.
+ if pos is None:
+ pos = {v: [seed.random() for i in range(dim)] for v in G}
+ # If no distance metric is provided, use Euclidean distance.
+ if metric is None:
+ metric = math.dist
+ nx.set_node_attributes(G, weight, weight_name)
+ nx.set_node_attributes(G, pos, pos_name)
+
+ # if p_dist is not supplied, use default r^-2
+ if p_dist is None:
+
+ def p_dist(r):
+ return r**-2
+
+ # Returns ``True`` if and only if the nodes whose attributes are
+ # ``du`` and ``dv`` should be joined, according to the threshold
+ # condition.
+ def should_join(pair):
+ u, v = pair
+ u_pos, v_pos = pos[u], pos[v]
+ u_weight, v_weight = weight[u], weight[v]
+ return (u_weight + v_weight) * p_dist(metric(u_pos, v_pos)) >= theta
+
+ G.add_edges_from(filter(should_join, combinations(G, 2)))
+ return G
+
+
+@py_random_state(6)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def waxman_graph(
+ n,
+ beta=0.4,
+ alpha=0.1,
+ L=None,
+ domain=(0, 0, 1, 1),
+ metric=None,
+ seed=None,
+ *,
+ pos_name="pos",
+):
+ r"""Returns a Waxman random graph.
+
+ The Waxman random graph model places `n` nodes uniformly at random
+ in a rectangular domain. Each pair of nodes at distance `d` is
+ joined by an edge with probability
+
+ .. math::
+ p = \beta \exp(-d / \alpha L).
+
+ This function implements both Waxman models, using the `L` keyword
+ argument.
+
+ * Waxman-1: if `L` is not specified, it is set to be the maximum distance
+ between any pair of nodes.
+ * Waxman-2: if `L` is specified, the distance between a pair of nodes is
+ chosen uniformly at random from the interval `[0, L]`.
+
+ Parameters
+ ----------
+ n : int or iterable
+ Number of nodes or iterable of nodes
+ beta: float
+ Model parameter
+ alpha: float
+ Model parameter
+ L : float, optional
+ Maximum distance between nodes. If not specified, the actual distance
+ is calculated.
+ domain : four-tuple of numbers, optional
+ Domain size, given as a tuple of the form `(x_min, y_min, x_max,
+ y_max)`.
+ metric : function
+ A metric on vectors of numbers (represented as lists or
+ tuples). This must be a function that accepts two lists (or
+ tuples) as input and yields a number as output. The function
+ must also satisfy the four requirements of a `metric`_.
+ Specifically, if $d$ is the function and $x$, $y$,
+ and $z$ are vectors in the graph, then $d$ must satisfy
+
+ 1. $d(x, y) \ge 0$,
+ 2. $d(x, y) = 0$ if and only if $x = y$,
+ 3. $d(x, y) = d(y, x)$,
+ 4. $d(x, z) \le d(x, y) + d(y, z)$.
+
+ If this argument is not specified, the Euclidean distance metric is
+ used.
+
+ .. _metric: https://en.wikipedia.org/wiki/Metric_%28mathematics%29
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ pos_name : string, default="pos"
+ The name of the node attribute which represents the position
+ in 2D coordinates of the node in the returned graph.
+
+ Returns
+ -------
+ Graph
+ A random Waxman graph, undirected and without self-loops. Each
+ node has a node attribute ``'pos'`` that stores the position of
+ that node in Euclidean space as generated by this function.
+
+ Examples
+ --------
+ Specify an alternate distance metric using the ``metric`` keyword
+ argument. For example, to use the "`taxicab metric`_" instead of the
+ default `Euclidean metric`_::
+
+ >>> dist = lambda x, y: sum(abs(a - b) for a, b in zip(x, y))
+ >>> G = nx.waxman_graph(10, 0.5, 0.1, metric=dist)
+
+ .. _taxicab metric: https://en.wikipedia.org/wiki/Taxicab_geometry
+ .. _Euclidean metric: https://en.wikipedia.org/wiki/Euclidean_distance
+
+ Notes
+ -----
+ Starting in NetworkX 2.0 the parameters alpha and beta align with their
+ usual roles in the probability distribution. In earlier versions their
+ positions in the expression were reversed. Their position in the calling
+ sequence reversed as well to minimize backward incompatibility.
+
+ References
+ ----------
+ .. [1] B. M. Waxman, *Routing of multipoint connections*.
+ IEEE J. Select. Areas Commun. 6(9),(1988) 1617--1622.
+ """
+ G = nx.empty_graph(n)
+ (xmin, ymin, xmax, ymax) = domain
+ # Each node gets a uniformly random position in the given rectangle.
+ pos = {v: (seed.uniform(xmin, xmax), seed.uniform(ymin, ymax)) for v in G}
+ nx.set_node_attributes(G, pos, pos_name)
+ # If no distance metric is provided, use Euclidean distance.
+ if metric is None:
+ metric = math.dist
+ # If the maximum distance L is not specified (that is, we are in the
+ # Waxman-1 model), then find the maximum distance between any pair
+ # of nodes.
+ #
+ # In the Waxman-1 model, join nodes randomly based on distance. In
+ # the Waxman-2 model, join randomly based on random l.
+ if L is None:
+ L = max(metric(x, y) for x, y in combinations(pos.values(), 2))
+
+ def dist(u, v):
+ return metric(pos[u], pos[v])
+
+ else:
+
+ def dist(u, v):
+ return seed.random() * L
+
+ # `pair` is the pair of nodes to decide whether to join.
+ def should_join(pair):
+ return seed.random() < beta * math.exp(-dist(*pair) / (alpha * L))
+
+ G.add_edges_from(filter(should_join, combinations(G, 2)))
+ return G
+
+
+@py_random_state(5)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def navigable_small_world_graph(n, p=1, q=1, r=2, dim=2, seed=None):
+ r"""Returns a navigable small-world graph.
+
+ A navigable small-world graph is a directed grid with additional long-range
+ connections that are chosen randomly.
+
+ [...] we begin with a set of nodes [...] that are identified with the set
+ of lattice points in an $n \times n$ square,
+ $\{(i, j): i \in \{1, 2, \ldots, n\}, j \in \{1, 2, \ldots, n\}\}$,
+ and we define the *lattice distance* between two nodes $(i, j)$ and
+ $(k, l)$ to be the number of "lattice steps" separating them:
+ $d((i, j), (k, l)) = |k - i| + |l - j|$.
+
+ For a universal constant $p >= 1$, the node $u$ has a directed edge to
+ every other node within lattice distance $p$---these are its *local
+ contacts*. For universal constants $q >= 0$ and $r >= 0$ we also
+ construct directed edges from $u$ to $q$ other nodes (the *long-range
+ contacts*) using independent random trials; the $i$th directed edge from
+ $u$ has endpoint $v$ with probability proportional to $[d(u,v)]^{-r}$.
+
+ -- [1]_
+
+ Parameters
+ ----------
+ n : int
+ The length of one side of the lattice; the number of nodes in
+ the graph is therefore $n^2$.
+ p : int
+ The diameter of short range connections. Each node is joined with every
+ other node within this lattice distance.
+ q : int
+ The number of long-range connections for each node.
+ r : float
+ Exponent for decaying probability of connections. The probability of
+ connecting to a node at lattice distance $d$ is $1/d^r$.
+ dim : int
+ Dimension of grid
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ References
+ ----------
+ .. [1] J. Kleinberg. The small-world phenomenon: An algorithmic
+ perspective. Proc. 32nd ACM Symposium on Theory of Computing, 2000.
+ """
+ if p < 1:
+ raise nx.NetworkXException("p must be >= 1")
+ if q < 0:
+ raise nx.NetworkXException("q must be >= 0")
+ if r < 0:
+ raise nx.NetworkXException("r must be >= 0")
+
+ G = nx.DiGraph()
+ nodes = list(product(range(n), repeat=dim))
+ for p1 in nodes:
+ probs = [0]
+ for p2 in nodes:
+ if p1 == p2:
+ continue
+ d = sum((abs(b - a) for a, b in zip(p1, p2)))
+ if d <= p:
+ G.add_edge(p1, p2)
+ probs.append(d**-r)
+ cdf = list(accumulate(probs))
+ for _ in range(q):
+ target = nodes[bisect_left(cdf, seed.uniform(0, cdf[-1]))]
+ G.add_edge(p1, target)
+ return G
+
+
+@py_random_state(7)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def thresholded_random_geometric_graph(
+ n,
+ radius,
+ theta,
+ dim=2,
+ pos=None,
+ weight=None,
+ p=2,
+ seed=None,
+ *,
+ pos_name="pos",
+ weight_name="weight",
+):
+ r"""Returns a thresholded random geometric graph in the unit cube.
+
+ The thresholded random geometric graph [1] model places `n` nodes
+ uniformly at random in the unit cube of dimensions `dim`. Each node
+ `u` is assigned a weight :math:`w_u`. Two nodes `u` and `v` are
+ joined by an edge if they are within the maximum connection distance,
+ `radius` computed by the `p`-Minkowski distance and the summation of
+ weights :math:`w_u` + :math:`w_v` is greater than or equal
+ to the threshold parameter `theta`.
+
+ Edges within `radius` of each other are determined using a KDTree when
+ SciPy is available. This reduces the time complexity from :math:`O(n^2)`
+ to :math:`O(n)`.
+
+ Parameters
+ ----------
+ n : int or iterable
+ Number of nodes or iterable of nodes
+ radius: float
+ Distance threshold value
+ theta: float
+ Threshold value
+ dim : int, optional
+ Dimension of graph
+ pos : dict, optional
+ A dictionary keyed by node with node positions as values.
+ weight : dict, optional
+ Node weights as a dictionary of numbers keyed by node.
+ p : float, optional (default 2)
+ Which Minkowski distance metric to use. `p` has to meet the condition
+ ``1 <= p <= infinity``.
+
+ If this argument is not specified, the :math:`L^2` metric
+ (the Euclidean distance metric), p = 2 is used.
+
+ This should not be confused with the `p` of an Erdős-Rényi random
+ graph, which represents probability.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ pos_name : string, default="pos"
+ The name of the node attribute which represents the position
+ in 2D coordinates of the node in the returned graph.
+ weight_name : string, default="weight"
+ The name of the node attribute which represents the weight
+ of the node in the returned graph.
+
+ Returns
+ -------
+ Graph
+ A thresholded random geographic graph, undirected and without
+ self-loops.
+
+ Each node has a node attribute ``'pos'`` that stores the
+ position of that node in Euclidean space as provided by the
+ ``pos`` keyword argument or, if ``pos`` was not provided, as
+ generated by this function. Similarly, each node has a nodethre
+ attribute ``'weight'`` that stores the weight of that node as
+ provided or as generated.
+
+ Examples
+ --------
+ Default Graph:
+
+ G = nx.thresholded_random_geometric_graph(50, 0.2, 0.1)
+
+ Custom Graph:
+
+ Create a thresholded random geometric graph on 50 uniformly distributed
+ nodes where nodes are joined by an edge if their sum weights drawn from
+ a exponential distribution with rate = 5 are >= theta = 0.1 and their
+ Euclidean distance is at most 0.2.
+
+ Notes
+ -----
+ This uses a *k*-d tree to build the graph.
+
+ The `pos` keyword argument can be used to specify node positions so you
+ can create an arbitrary distribution and domain for positions.
+
+ For example, to use a 2D Gaussian distribution of node positions with mean
+ (0, 0) and standard deviation 2
+
+ If weights are not specified they are assigned to nodes by drawing randomly
+ from the exponential distribution with rate parameter :math:`\lambda=1`.
+ To specify weights from a different distribution, use the `weight` keyword
+ argument::
+
+ ::
+
+ >>> import random
+ >>> import math
+ >>> n = 50
+ >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)}
+ >>> w = {i: random.expovariate(5.0) for i in range(n)}
+ >>> G = nx.thresholded_random_geometric_graph(n, 0.2, 0.1, 2, pos, w)
+
+ References
+ ----------
+ .. [1] http://cole-maclean.github.io/blog/files/thesis.pdf
+
+ """
+ G = nx.empty_graph(n)
+ G.name = f"thresholded_random_geometric_graph({n}, {radius}, {theta}, {dim})"
+ # If no weights are provided, choose them from an exponential
+ # distribution.
+ if weight is None:
+ weight = {v: seed.expovariate(1) for v in G}
+ # If no positions are provided, choose uniformly random vectors in
+ # Euclidean space of the specified dimension.
+ if pos is None:
+ pos = {v: [seed.random() for i in range(dim)] for v in G}
+ # If no distance metric is provided, use Euclidean distance.
+ nx.set_node_attributes(G, weight, weight_name)
+ nx.set_node_attributes(G, pos, pos_name)
+
+ edges = (
+ (u, v)
+ for u, v in _geometric_edges(G, radius, p, pos_name)
+ if weight[u] + weight[v] >= theta
+ )
+ G.add_edges_from(edges)
+ return G
+
+
+@py_random_state(5)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def geometric_soft_configuration_graph(
+ *, beta, n=None, gamma=None, mean_degree=None, kappas=None, seed=None
+):
+ r"""Returns a random graph from the geometric soft configuration model.
+
+ The $\mathbb{S}^1$ model [1]_ is the geometric soft configuration model
+ which is able to explain many fundamental features of real networks such as
+ small-world property, heteregenous degree distributions, high level of
+ clustering, and self-similarity.
+
+ In the geometric soft configuration model, a node $i$ is assigned two hidden
+ variables: a hidden degree $\kappa_i$, quantifying its popularity, influence,
+ or importance, and an angular position $\theta_i$ in a circle abstracting the
+ similarity space, where angular distances between nodes are a proxy for their
+ similarity. Focusing on the angular position, this model is often called
+ the $\mathbb{S}^1$ model (a one-dimensional sphere). The circle's radius is
+ adjusted to $R = N/2\pi$, where $N$ is the number of nodes, so that the density
+ is set to 1 without loss of generality.
+
+ The connection probability between any pair of nodes increases with
+ the product of their hidden degrees (i.e., their combined popularities),
+ and decreases with the angular distance between the two nodes.
+ Specifically, nodes $i$ and $j$ are connected with the probability
+
+ $p_{ij} = \frac{1}{1 + \frac{d_{ij}^\beta}{\left(\mu \kappa_i \kappa_j\right)^{\max(1, \beta)}}}$
+
+ where $d_{ij} = R\Delta\theta_{ij}$ is the arc length of the circle between
+ nodes $i$ and $j$ separated by an angular distance $\Delta\theta_{ij}$.
+ Parameters $\mu$ and $\beta$ (also called inverse temperature) control the
+ average degree and the clustering coefficient, respectively.
+
+ It can be shown [2]_ that the model undergoes a structural phase transition
+ at $\beta=1$ so that for $\beta<1$ networks are unclustered in the thermodynamic
+ limit (when $N\to \infty$) whereas for $\beta>1$ the ensemble generates
+ networks with finite clustering coefficient.
+
+ The $\mathbb{S}^1$ model can be expressed as a purely geometric model
+ $\mathbb{H}^2$ in the hyperbolic plane [3]_ by mapping the hidden degree of
+ each node into a radial coordinate as
+
+ $r_i = \hat{R} - \frac{2 \max(1, \beta)}{\beta \zeta} \ln \left(\frac{\kappa_i}{\kappa_0}\right)$
+
+ where $\hat{R}$ is the radius of the hyperbolic disk and $\zeta$ is the curvature,
+
+ $\hat{R} = \frac{2}{\zeta} \ln \left(\frac{N}{\pi}\right)
+ - \frac{2\max(1, \beta)}{\beta \zeta} \ln (\mu \kappa_0^2)$
+
+ The connection probability then reads
+
+ $p_{ij} = \frac{1}{1 + \exp\left({\frac{\beta\zeta}{2} (x_{ij} - \hat{R})}\right)}$
+
+ where
+
+ $x_{ij} = r_i + r_j + \frac{2}{\zeta} \ln \frac{\Delta\theta_{ij}}{2}$
+
+ is a good approximation of the hyperbolic distance between two nodes separated
+ by an angular distance $\Delta\theta_{ij}$ with radial coordinates $r_i$ and $r_j$.
+ For $\beta > 1$, the curvature $\zeta = 1$, for $\beta < 1$, $\zeta = \beta^{-1}$.
+
+
+ Parameters
+ ----------
+ Either `n`, `gamma`, `mean_degree` are provided or `kappas`. The values of
+ `n`, `gamma`, `mean_degree` (if provided) are used to construct a random
+ kappa-dict keyed by node with values sampled from a power-law distribution.
+
+ beta : positive number
+ Inverse temperature, controlling the clustering coefficient.
+ n : int (default: None)
+ Size of the network (number of nodes).
+ If not provided, `kappas` must be provided and holds the nodes.
+ gamma : float (default: None)
+ Exponent of the power-law distribution for hidden degrees `kappas`.
+ If not provided, `kappas` must be provided directly.
+ mean_degree : float (default: None)
+ The mean degree in the network.
+ If not provided, `kappas` must be provided directly.
+ kappas : dict (default: None)
+ A dict keyed by node to its hidden degree value.
+ If not provided, random values are computed based on a power-law
+ distribution using `n`, `gamma` and `mean_degree`.
+ seed : int, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ Graph
+ A random geometric soft configuration graph (undirected with no self-loops).
+ Each node has three node-attributes:
+
+ - ``kappa`` that represents the hidden degree.
+
+ - ``theta`` the position in the similarity space ($\mathbb{S}^1$) which is
+ also the angular position in the hyperbolic plane.
+
+ - ``radius`` the radial position in the hyperbolic plane
+ (based on the hidden degree).
+
+
+ Examples
+ --------
+ Generate a network with specified parameters:
+
+ >>> G = nx.geometric_soft_configuration_graph(
+ ... beta=1.5, n=100, gamma=2.7, mean_degree=5
+ ... )
+
+ Create a geometric soft configuration graph with 100 nodes. The $\beta$ parameter
+ is set to 1.5 and the exponent of the powerlaw distribution of the hidden
+ degrees is 2.7 with mean value of 5.
+
+ Generate a network with predefined hidden degrees:
+
+ >>> kappas = {i: 10 for i in range(100)}
+ >>> G = nx.geometric_soft_configuration_graph(beta=2.5, kappas=kappas)
+
+ Create a geometric soft configuration graph with 100 nodes. The $\beta$ parameter
+ is set to 2.5 and all nodes with hidden degree $\kappa=10$.
+
+
+ References
+ ----------
+ .. [1] Serrano, M. Á., Krioukov, D., & Boguñá, M. (2008). Self-similarity
+ of complex networks and hidden metric spaces. Physical review letters, 100(7), 078701.
+
+ .. [2] van der Kolk, J., Serrano, M. Á., & Boguñá, M. (2022). An anomalous
+ topological phase transition in spatial random graphs. Communications Physics, 5(1), 245.
+
+ .. [3] Krioukov, D., Papadopoulos, F., Kitsak, M., Vahdat, A., & Boguná, M. (2010).
+ Hyperbolic geometry of complex networks. Physical Review E, 82(3), 036106.
+
+ """
+ if beta <= 0:
+ raise nx.NetworkXError("The parameter beta cannot be smaller or equal to 0.")
+
+ if kappas is not None:
+ if not all((n is None, gamma is None, mean_degree is None)):
+ raise nx.NetworkXError(
+ "When kappas is input, n, gamma and mean_degree must not be."
+ )
+
+ n = len(kappas)
+ mean_degree = sum(kappas) / len(kappas)
+ else:
+ if any((n is None, gamma is None, mean_degree is None)):
+ raise nx.NetworkXError(
+ "Please provide either kappas, or all 3 of: n, gamma and mean_degree."
+ )
+
+ # Generate `n` hidden degrees from a powerlaw distribution
+ # with given exponent `gamma` and mean value `mean_degree`
+ gam_ratio = (gamma - 2) / (gamma - 1)
+ kappa_0 = mean_degree * gam_ratio * (1 - 1 / n) / (1 - 1 / n**gam_ratio)
+ base = 1 - 1 / n
+ power = 1 / (1 - gamma)
+ kappas = {i: kappa_0 * (1 - seed.random() * base) ** power for i in range(n)}
+
+ G = nx.Graph()
+ R = n / (2 * math.pi)
+
+ # Approximate values for mu in the thermodynamic limit (when n -> infinity)
+ if beta > 1:
+ mu = beta * math.sin(math.pi / beta) / (2 * math.pi * mean_degree)
+ elif beta == 1:
+ mu = 1 / (2 * mean_degree * math.log(n))
+ else:
+ mu = (1 - beta) / (2**beta * mean_degree * n ** (1 - beta))
+
+ # Generate random positions on a circle
+ thetas = {k: seed.uniform(0, 2 * math.pi) for k in kappas}
+
+ for u in kappas:
+ for v in list(G):
+ angle = math.pi - math.fabs(math.pi - math.fabs(thetas[u] - thetas[v]))
+ dij = math.pow(R * angle, beta)
+ mu_kappas = math.pow(mu * kappas[u] * kappas[v], max(1, beta))
+ p_ij = 1 / (1 + dij / mu_kappas)
+
+ # Create an edge with a certain connection probability
+ if seed.random() < p_ij:
+ G.add_edge(u, v)
+ G.add_node(u)
+
+ nx.set_node_attributes(G, thetas, "theta")
+ nx.set_node_attributes(G, kappas, "kappa")
+
+ # Map hidden degrees into the radial coordinates
+ zeta = 1 if beta > 1 else 1 / beta
+ kappa_min = min(kappas.values())
+ R_c = 2 * max(1, beta) / (beta * zeta)
+ R_hat = (2 / zeta) * math.log(n / math.pi) - R_c * math.log(mu * kappa_min)
+ radii = {node: R_hat - R_c * math.log(kappa) for node, kappa in kappas.items()}
+ nx.set_node_attributes(G, radii, "radius")
+
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/harary_graph.py b/.venv/lib/python3.12/site-packages/networkx/generators/harary_graph.py
new file mode 100644
index 00000000..591587d3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/harary_graph.py
@@ -0,0 +1,199 @@
+"""Generators for Harary graphs
+
+This module gives two generators for the Harary graph, which was
+introduced by the famous mathematician Frank Harary in his 1962 work [H]_.
+The first generator gives the Harary graph that maximizes the node
+connectivity with given number of nodes and given number of edges.
+The second generator gives the Harary graph that minimizes
+the number of edges in the graph with given node connectivity and
+number of nodes.
+
+References
+----------
+.. [H] Harary, F. "The Maximum Connectivity of a Graph."
+ Proc. Nat. Acad. Sci. USA 48, 1142-1146, 1962.
+
+"""
+
+import networkx as nx
+from networkx.exception import NetworkXError
+
+__all__ = ["hnm_harary_graph", "hkn_harary_graph"]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def hnm_harary_graph(n, m, create_using=None):
+ """Returns the Harary graph with given numbers of nodes and edges.
+
+ The Harary graph $H_{n,m}$ is the graph that maximizes node connectivity
+ with $n$ nodes and $m$ edges.
+
+ This maximum node connectivity is known to be floor($2m/n$). [1]_
+
+ Parameters
+ ----------
+ n: integer
+ The number of nodes the generated graph is to contain
+
+ m: integer
+ The number of edges the generated graph is to contain
+
+ create_using : NetworkX graph constructor, optional Graph type
+ to create (default=nx.Graph). If graph instance, then cleared
+ before populated.
+
+ Returns
+ -------
+ NetworkX graph
+ The Harary graph $H_{n,m}$.
+
+ See Also
+ --------
+ hkn_harary_graph
+
+ Notes
+ -----
+ This algorithm runs in $O(m)$ time.
+ It is implemented by following the Reference [2]_.
+
+ References
+ ----------
+ .. [1] F. T. Boesch, A. Satyanarayana, and C. L. Suffel,
+ "A Survey of Some Network Reliability Analysis and Synthesis Results,"
+ Networks, pp. 99-107, 2009.
+
+ .. [2] Harary, F. "The Maximum Connectivity of a Graph."
+ Proc. Nat. Acad. Sci. USA 48, 1142-1146, 1962.
+ """
+
+ if n < 1:
+ raise NetworkXError("The number of nodes must be >= 1!")
+ if m < n - 1:
+ raise NetworkXError("The number of edges must be >= n - 1 !")
+ if m > n * (n - 1) // 2:
+ raise NetworkXError("The number of edges must be <= n(n-1)/2")
+
+ # Construct an empty graph with n nodes first
+ H = nx.empty_graph(n, create_using)
+ # Get the floor of average node degree
+ d = 2 * m // n
+
+ # Test the parity of n and d
+ if (n % 2 == 0) or (d % 2 == 0):
+ # Start with a regular graph of d degrees
+ offset = d // 2
+ for i in range(n):
+ for j in range(1, offset + 1):
+ H.add_edge(i, (i - j) % n)
+ H.add_edge(i, (i + j) % n)
+ if d & 1:
+ # in case d is odd; n must be even in this case
+ half = n // 2
+ for i in range(half):
+ # add edges diagonally
+ H.add_edge(i, i + half)
+ # Get the remainder of 2*m modulo n
+ r = 2 * m % n
+ if r > 0:
+ # add remaining edges at offset+1
+ for i in range(r // 2):
+ H.add_edge(i, i + offset + 1)
+ else:
+ # Start with a regular graph of (d - 1) degrees
+ offset = (d - 1) // 2
+ for i in range(n):
+ for j in range(1, offset + 1):
+ H.add_edge(i, (i - j) % n)
+ H.add_edge(i, (i + j) % n)
+ half = n // 2
+ for i in range(m - n * offset):
+ # add the remaining m - n*offset edges between i and i+half
+ H.add_edge(i, (i + half) % n)
+
+ return H
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def hkn_harary_graph(k, n, create_using=None):
+ """Returns the Harary graph with given node connectivity and node number.
+
+ The Harary graph $H_{k,n}$ is the graph that minimizes the number of
+ edges needed with given node connectivity $k$ and node number $n$.
+
+ This smallest number of edges is known to be ceil($kn/2$) [1]_.
+
+ Parameters
+ ----------
+ k: integer
+ The node connectivity of the generated graph
+
+ n: integer
+ The number of nodes the generated graph is to contain
+
+ create_using : NetworkX graph constructor, optional Graph type
+ to create (default=nx.Graph). If graph instance, then cleared
+ before populated.
+
+ Returns
+ -------
+ NetworkX graph
+ The Harary graph $H_{k,n}$.
+
+ See Also
+ --------
+ hnm_harary_graph
+
+ Notes
+ -----
+ This algorithm runs in $O(kn)$ time.
+ It is implemented by following the Reference [2]_.
+
+ References
+ ----------
+ .. [1] Weisstein, Eric W. "Harary Graph." From MathWorld--A Wolfram Web
+ Resource. http://mathworld.wolfram.com/HararyGraph.html.
+
+ .. [2] Harary, F. "The Maximum Connectivity of a Graph."
+ Proc. Nat. Acad. Sci. USA 48, 1142-1146, 1962.
+ """
+
+ if k < 1:
+ raise NetworkXError("The node connectivity must be >= 1!")
+ if n < k + 1:
+ raise NetworkXError("The number of nodes must be >= k+1 !")
+
+ # in case of connectivity 1, simply return the path graph
+ if k == 1:
+ H = nx.path_graph(n, create_using)
+ return H
+
+ # Construct an empty graph with n nodes first
+ H = nx.empty_graph(n, create_using)
+
+ # Test the parity of k and n
+ if (k % 2 == 0) or (n % 2 == 0):
+ # Construct a regular graph with k degrees
+ offset = k // 2
+ for i in range(n):
+ for j in range(1, offset + 1):
+ H.add_edge(i, (i - j) % n)
+ H.add_edge(i, (i + j) % n)
+ if k & 1:
+ # odd degree; n must be even in this case
+ half = n // 2
+ for i in range(half):
+ # add edges diagonally
+ H.add_edge(i, i + half)
+ else:
+ # Construct a regular graph with (k - 1) degrees
+ offset = (k - 1) // 2
+ for i in range(n):
+ for j in range(1, offset + 1):
+ H.add_edge(i, (i - j) % n)
+ H.add_edge(i, (i + j) % n)
+ half = n // 2
+ for i in range(half + 1):
+ # add half+1 edges between i and i+half
+ H.add_edge(i, (i + half) % n)
+
+ return H
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/internet_as_graphs.py b/.venv/lib/python3.12/site-packages/networkx/generators/internet_as_graphs.py
new file mode 100644
index 00000000..449d5437
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/internet_as_graphs.py
@@ -0,0 +1,441 @@
+"""Generates graphs resembling the Internet Autonomous System network"""
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+__all__ = ["random_internet_as_graph"]
+
+
+def uniform_int_from_avg(a, m, seed):
+ """Pick a random integer with uniform probability.
+
+ Returns a random integer uniformly taken from a distribution with
+ minimum value 'a' and average value 'm', X~U(a,b), E[X]=m, X in N where
+ b = 2*m - a.
+
+ Notes
+ -----
+ p = (b-floor(b))/2
+ X = X1 + X2; X1~U(a,floor(b)), X2~B(p)
+ E[X] = E[X1] + E[X2] = (floor(b)+a)/2 + (b-floor(b))/2 = (b+a)/2 = m
+ """
+
+ from math import floor
+
+ assert m >= a
+ b = 2 * m - a
+ p = (b - floor(b)) / 2
+ X1 = round(seed.random() * (floor(b) - a) + a)
+ if seed.random() < p:
+ X2 = 1
+ else:
+ X2 = 0
+ return X1 + X2
+
+
+def choose_pref_attach(degs, seed):
+ """Pick a random value, with a probability given by its weight.
+
+ Returns a random choice among degs keys, each of which has a
+ probability proportional to the corresponding dictionary value.
+
+ Parameters
+ ----------
+ degs: dictionary
+ It contains the possible values (keys) and the corresponding
+ probabilities (values)
+ seed: random state
+
+ Returns
+ -------
+ v: object
+ A key of degs or None if degs is empty
+ """
+
+ if len(degs) == 0:
+ return None
+ s = sum(degs.values())
+ if s == 0:
+ return seed.choice(list(degs.keys()))
+ v = seed.random() * s
+
+ nodes = list(degs.keys())
+ i = 0
+ acc = degs[nodes[i]]
+ while v > acc:
+ i += 1
+ acc += degs[nodes[i]]
+ return nodes[i]
+
+
+class AS_graph_generator:
+ """Generates random internet AS graphs."""
+
+ def __init__(self, n, seed):
+ """Initializes variables. Immediate numbers are taken from [1].
+
+ Parameters
+ ----------
+ n: integer
+ Number of graph nodes
+ seed: random state
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ GG: AS_graph_generator object
+
+ References
+ ----------
+ [1] A. Elmokashfi, A. Kvalbein and C. Dovrolis, "On the Scalability of
+ BGP: The Role of Topology Growth," in IEEE Journal on Selected Areas
+ in Communications, vol. 28, no. 8, pp. 1250-1261, October 2010.
+ """
+
+ self.seed = seed
+ self.n_t = min(n, round(self.seed.random() * 2 + 4)) # num of T nodes
+ self.n_m = round(0.15 * n) # number of M nodes
+ self.n_cp = round(0.05 * n) # number of CP nodes
+ self.n_c = max(0, n - self.n_t - self.n_m - self.n_cp) # number of C nodes
+
+ self.d_m = 2 + (2.5 * n) / 10000 # average multihoming degree for M nodes
+ self.d_cp = 2 + (1.5 * n) / 10000 # avg multihoming degree for CP nodes
+ self.d_c = 1 + (5 * n) / 100000 # average multihoming degree for C nodes
+
+ self.p_m_m = 1 + (2 * n) / 10000 # avg num of peer edges between M and M
+ self.p_cp_m = 0.2 + (2 * n) / 10000 # avg num of peer edges between CP, M
+ self.p_cp_cp = 0.05 + (2 * n) / 100000 # avg num of peer edges btwn CP, CP
+
+ self.t_m = 0.375 # probability M's provider is T
+ self.t_cp = 0.375 # probability CP's provider is T
+ self.t_c = 0.125 # probability C's provider is T
+
+ def t_graph(self):
+ """Generates the core mesh network of tier one nodes of a AS graph.
+
+ Returns
+ -------
+ G: Networkx Graph
+ Core network
+ """
+
+ self.G = nx.Graph()
+ for i in range(self.n_t):
+ self.G.add_node(i, type="T")
+ for r in self.regions:
+ self.regions[r].add(i)
+ for j in self.G.nodes():
+ if i != j:
+ self.add_edge(i, j, "peer")
+ self.customers[i] = set()
+ self.providers[i] = set()
+ return self.G
+
+ def add_edge(self, i, j, kind):
+ if kind == "transit":
+ customer = str(i)
+ else:
+ customer = "none"
+ self.G.add_edge(i, j, type=kind, customer=customer)
+
+ def choose_peer_pref_attach(self, node_list):
+ """Pick a node with a probability weighted by its peer degree.
+
+ Pick a node from node_list with preferential attachment
+ computed only on their peer degree
+ """
+
+ d = {}
+ for n in node_list:
+ d[n] = self.G.nodes[n]["peers"]
+ return choose_pref_attach(d, self.seed)
+
+ def choose_node_pref_attach(self, node_list):
+ """Pick a node with a probability weighted by its degree.
+
+ Pick a node from node_list with preferential attachment
+ computed on their degree
+ """
+
+ degs = dict(self.G.degree(node_list))
+ return choose_pref_attach(degs, self.seed)
+
+ def add_customer(self, i, j):
+ """Keep the dictionaries 'customers' and 'providers' consistent."""
+
+ self.customers[j].add(i)
+ self.providers[i].add(j)
+ for z in self.providers[j]:
+ self.customers[z].add(i)
+ self.providers[i].add(z)
+
+ def add_node(self, i, kind, reg2prob, avg_deg, t_edge_prob):
+ """Add a node and its customer transit edges to the graph.
+
+ Parameters
+ ----------
+ i: object
+ Identifier of the new node
+ kind: string
+ Type of the new node. Options are: 'M' for middle node, 'CP' for
+ content provider and 'C' for customer.
+ reg2prob: float
+ Probability the new node can be in two different regions.
+ avg_deg: float
+ Average number of transit nodes of which node i is customer.
+ t_edge_prob: float
+ Probability node i establish a customer transit edge with a tier
+ one (T) node
+
+ Returns
+ -------
+ i: object
+ Identifier of the new node
+ """
+
+ regs = 1 # regions in which node resides
+ if self.seed.random() < reg2prob: # node is in two regions
+ regs = 2
+ node_options = set()
+
+ self.G.add_node(i, type=kind, peers=0)
+ self.customers[i] = set()
+ self.providers[i] = set()
+ self.nodes[kind].add(i)
+ for r in self.seed.sample(list(self.regions), regs):
+ node_options = node_options.union(self.regions[r])
+ self.regions[r].add(i)
+
+ edge_num = uniform_int_from_avg(1, avg_deg, self.seed)
+
+ t_options = node_options.intersection(self.nodes["T"])
+ m_options = node_options.intersection(self.nodes["M"])
+ if i in m_options:
+ m_options.remove(i)
+ d = 0
+ while d < edge_num and (len(t_options) > 0 or len(m_options) > 0):
+ if len(m_options) == 0 or (
+ len(t_options) > 0 and self.seed.random() < t_edge_prob
+ ): # add edge to a T node
+ j = self.choose_node_pref_attach(t_options)
+ t_options.remove(j)
+ else:
+ j = self.choose_node_pref_attach(m_options)
+ m_options.remove(j)
+ self.add_edge(i, j, "transit")
+ self.add_customer(i, j)
+ d += 1
+
+ return i
+
+ def add_m_peering_link(self, m, to_kind):
+ """Add a peering link between two middle tier (M) nodes.
+
+ Target node j is drawn considering a preferential attachment based on
+ other M node peering degree.
+
+ Parameters
+ ----------
+ m: object
+ Node identifier
+ to_kind: string
+ type for target node j (must be always M)
+
+ Returns
+ -------
+ success: boolean
+ """
+
+ # candidates are of type 'M' and are not customers of m
+ node_options = self.nodes["M"].difference(self.customers[m])
+ # candidates are not providers of m
+ node_options = node_options.difference(self.providers[m])
+ # remove self
+ if m in node_options:
+ node_options.remove(m)
+
+ # remove candidates we are already connected to
+ for j in self.G.neighbors(m):
+ if j in node_options:
+ node_options.remove(j)
+
+ if len(node_options) > 0:
+ j = self.choose_peer_pref_attach(node_options)
+ self.add_edge(m, j, "peer")
+ self.G.nodes[m]["peers"] += 1
+ self.G.nodes[j]["peers"] += 1
+ return True
+ else:
+ return False
+
+ def add_cp_peering_link(self, cp, to_kind):
+ """Add a peering link to a content provider (CP) node.
+
+ Target node j can be CP or M and it is drawn uniformly among the nodes
+ belonging to the same region as cp.
+
+ Parameters
+ ----------
+ cp: object
+ Node identifier
+ to_kind: string
+ type for target node j (must be M or CP)
+
+ Returns
+ -------
+ success: boolean
+ """
+
+ node_options = set()
+ for r in self.regions: # options include nodes in the same region(s)
+ if cp in self.regions[r]:
+ node_options = node_options.union(self.regions[r])
+
+ # options are restricted to the indicated kind ('M' or 'CP')
+ node_options = self.nodes[to_kind].intersection(node_options)
+
+ # remove self
+ if cp in node_options:
+ node_options.remove(cp)
+
+ # remove nodes that are cp's providers
+ node_options = node_options.difference(self.providers[cp])
+
+ # remove nodes we are already connected to
+ for j in self.G.neighbors(cp):
+ if j in node_options:
+ node_options.remove(j)
+
+ if len(node_options) > 0:
+ j = self.seed.sample(list(node_options), 1)[0]
+ self.add_edge(cp, j, "peer")
+ self.G.nodes[cp]["peers"] += 1
+ self.G.nodes[j]["peers"] += 1
+ return True
+ else:
+ return False
+
+ def graph_regions(self, rn):
+ """Initializes AS network regions.
+
+ Parameters
+ ----------
+ rn: integer
+ Number of regions
+ """
+
+ self.regions = {}
+ for i in range(rn):
+ self.regions["REG" + str(i)] = set()
+
+ def add_peering_links(self, from_kind, to_kind):
+ """Utility function to add peering links among node groups."""
+ peer_link_method = None
+ if from_kind == "M":
+ peer_link_method = self.add_m_peering_link
+ m = self.p_m_m
+ if from_kind == "CP":
+ peer_link_method = self.add_cp_peering_link
+ if to_kind == "M":
+ m = self.p_cp_m
+ else:
+ m = self.p_cp_cp
+
+ for i in self.nodes[from_kind]:
+ num = uniform_int_from_avg(0, m, self.seed)
+ for _ in range(num):
+ peer_link_method(i, to_kind)
+
+ def generate(self):
+ """Generates a random AS network graph as described in [1].
+
+ Returns
+ -------
+ G: Graph object
+
+ Notes
+ -----
+ The process steps are the following: first we create the core network
+ of tier one nodes, then we add the middle tier (M), the content
+ provider (CP) and the customer (C) nodes along with their transit edges
+ (link i,j means i is customer of j). Finally we add peering links
+ between M nodes, between M and CP nodes and between CP node couples.
+ For a detailed description of the algorithm, please refer to [1].
+
+ References
+ ----------
+ [1] A. Elmokashfi, A. Kvalbein and C. Dovrolis, "On the Scalability of
+ BGP: The Role of Topology Growth," in IEEE Journal on Selected Areas
+ in Communications, vol. 28, no. 8, pp. 1250-1261, October 2010.
+ """
+
+ self.graph_regions(5)
+ self.customers = {}
+ self.providers = {}
+ self.nodes = {"T": set(), "M": set(), "CP": set(), "C": set()}
+
+ self.t_graph()
+ self.nodes["T"] = set(self.G.nodes())
+
+ i = len(self.nodes["T"])
+ for _ in range(self.n_m):
+ self.nodes["M"].add(self.add_node(i, "M", 0.2, self.d_m, self.t_m))
+ i += 1
+ for _ in range(self.n_cp):
+ self.nodes["CP"].add(self.add_node(i, "CP", 0.05, self.d_cp, self.t_cp))
+ i += 1
+ for _ in range(self.n_c):
+ self.nodes["C"].add(self.add_node(i, "C", 0, self.d_c, self.t_c))
+ i += 1
+
+ self.add_peering_links("M", "M")
+ self.add_peering_links("CP", "M")
+ self.add_peering_links("CP", "CP")
+
+ return self.G
+
+
+@py_random_state(1)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_internet_as_graph(n, seed=None):
+ """Generates a random undirected graph resembling the Internet AS network
+
+ Parameters
+ ----------
+ n: integer in [1000, 10000]
+ Number of graph nodes
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G: Networkx Graph object
+ A randomly generated undirected graph
+
+ Notes
+ -----
+ This algorithm returns an undirected graph resembling the Internet
+ Autonomous System (AS) network, it uses the approach by Elmokashfi et al.
+ [1]_ and it grants the properties described in the related paper [1]_.
+
+ Each node models an autonomous system, with an attribute 'type' specifying
+ its kind; tier-1 (T), mid-level (M), customer (C) or content-provider (CP).
+ Each edge models an ADV communication link (hence, bidirectional) with
+ attributes:
+
+ - type: transit|peer, the kind of commercial agreement between nodes;
+ - customer: <node id>, the identifier of the node acting as customer
+ ('none' if type is peer).
+
+ References
+ ----------
+ .. [1] A. Elmokashfi, A. Kvalbein and C. Dovrolis, "On the Scalability of
+ BGP: The Role of Topology Growth," in IEEE Journal on Selected Areas
+ in Communications, vol. 28, no. 8, pp. 1250-1261, October 2010.
+ """
+
+ GG = AS_graph_generator(n, seed)
+ G = GG.generate()
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/intersection.py b/.venv/lib/python3.12/site-packages/networkx/generators/intersection.py
new file mode 100644
index 00000000..e63af5be
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/intersection.py
@@ -0,0 +1,125 @@
+"""
+Generators for random intersection graphs.
+"""
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+__all__ = [
+ "uniform_random_intersection_graph",
+ "k_random_intersection_graph",
+ "general_random_intersection_graph",
+]
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def uniform_random_intersection_graph(n, m, p, seed=None):
+ """Returns a uniform random intersection graph.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes in the first bipartite set (nodes)
+ m : int
+ The number of nodes in the second bipartite set (attributes)
+ p : float
+ Probability of connecting nodes between bipartite sets
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ See Also
+ --------
+ gnp_random_graph
+
+ References
+ ----------
+ .. [1] K.B. Singer-Cohen, Random Intersection Graphs, 1995,
+ PhD thesis, Johns Hopkins University
+ .. [2] Fill, J. A., Scheinerman, E. R., and Singer-Cohen, K. B.,
+ Random intersection graphs when m = !(n):
+ An equivalence theorem relating the evolution of the g(n, m, p)
+ and g(n, p) models. Random Struct. Algorithms 16, 2 (2000), 156–176.
+ """
+ from networkx.algorithms import bipartite
+
+ G = bipartite.random_graph(n, m, p, seed)
+ return nx.projected_graph(G, range(n))
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def k_random_intersection_graph(n, m, k, seed=None):
+ """Returns a intersection graph with randomly chosen attribute sets for
+ each node that are of equal size (k).
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes in the first bipartite set (nodes)
+ m : int
+ The number of nodes in the second bipartite set (attributes)
+ k : float
+ Size of attribute set to assign to each node.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ See Also
+ --------
+ gnp_random_graph, uniform_random_intersection_graph
+
+ References
+ ----------
+ .. [1] Godehardt, E., and Jaworski, J.
+ Two models of random intersection graphs and their applications.
+ Electronic Notes in Discrete Mathematics 10 (2001), 129--132.
+ """
+ G = nx.empty_graph(n + m)
+ mset = range(n, n + m)
+ for v in range(n):
+ targets = seed.sample(mset, k)
+ G.add_edges_from(zip([v] * len(targets), targets))
+ return nx.projected_graph(G, range(n))
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def general_random_intersection_graph(n, m, p, seed=None):
+ """Returns a random intersection graph with independent probabilities
+ for connections between node and attribute sets.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes in the first bipartite set (nodes)
+ m : int
+ The number of nodes in the second bipartite set (attributes)
+ p : list of floats of length m
+ Probabilities for connecting nodes to each attribute
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ See Also
+ --------
+ gnp_random_graph, uniform_random_intersection_graph
+
+ References
+ ----------
+ .. [1] Nikoletseas, S. E., Raptopoulos, C., and Spirakis, P. G.
+ The existence and efficient construction of large independent sets
+ in general random intersection graphs. In ICALP (2004), J. D´ıaz,
+ J. Karhum¨aki, A. Lepist¨o, and D. Sannella, Eds., vol. 3142
+ of Lecture Notes in Computer Science, Springer, pp. 1029–1040.
+ """
+ if len(p) != m:
+ raise ValueError("Probability list p must have m elements.")
+ G = nx.empty_graph(n + m)
+ mset = range(n, n + m)
+ for u in range(n):
+ for v, q in zip(mset, p):
+ if seed.random() < q:
+ G.add_edge(u, v)
+ return nx.projected_graph(G, range(n))
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/interval_graph.py b/.venv/lib/python3.12/site-packages/networkx/generators/interval_graph.py
new file mode 100644
index 00000000..6a3fda45
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/interval_graph.py
@@ -0,0 +1,70 @@
+"""
+Generators for interval graph.
+"""
+
+from collections.abc import Sequence
+
+import networkx as nx
+
+__all__ = ["interval_graph"]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def interval_graph(intervals):
+ """Generates an interval graph for a list of intervals given.
+
+ In graph theory, an interval graph is an undirected graph formed from a set
+ of closed intervals on the real line, with a vertex for each interval
+ and an edge between vertices whose intervals intersect.
+ It is the intersection graph of the intervals.
+
+ More information can be found at:
+ https://en.wikipedia.org/wiki/Interval_graph
+
+ Parameters
+ ----------
+ intervals : a sequence of intervals, say (l, r) where l is the left end,
+ and r is the right end of the closed interval.
+
+ Returns
+ -------
+ G : networkx graph
+
+ Examples
+ --------
+ >>> intervals = [(-2, 3), [1, 4], (2, 3), (4, 6)]
+ >>> G = nx.interval_graph(intervals)
+ >>> sorted(G.edges)
+ [((-2, 3), (1, 4)), ((-2, 3), (2, 3)), ((1, 4), (2, 3)), ((1, 4), (4, 6))]
+
+ Raises
+ ------
+ :exc:`TypeError`
+ if `intervals` contains None or an element which is not
+ collections.abc.Sequence or not a length of 2.
+ :exc:`ValueError`
+ if `intervals` contains an interval such that min1 > max1
+ where min1,max1 = interval
+ """
+ intervals = list(intervals)
+ for interval in intervals:
+ if not (isinstance(interval, Sequence) and len(interval) == 2):
+ raise TypeError(
+ "Each interval must have length 2, and be a "
+ "collections.abc.Sequence such as tuple or list."
+ )
+ if interval[0] > interval[1]:
+ raise ValueError(f"Interval must have lower value first. Got {interval}")
+
+ graph = nx.Graph()
+
+ tupled_intervals = [tuple(interval) for interval in intervals]
+ graph.add_nodes_from(tupled_intervals)
+
+ while tupled_intervals:
+ min1, max1 = interval1 = tupled_intervals.pop()
+ for interval2 in tupled_intervals:
+ min2, max2 = interval2
+ if max1 >= min2 and max2 >= min1:
+ graph.add_edge(interval1, interval2)
+ return graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/joint_degree_seq.py b/.venv/lib/python3.12/site-packages/networkx/generators/joint_degree_seq.py
new file mode 100644
index 00000000..c426df94
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/joint_degree_seq.py
@@ -0,0 +1,664 @@
+"""Generate graphs with a given joint degree and directed joint degree"""
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+__all__ = [
+ "is_valid_joint_degree",
+ "is_valid_directed_joint_degree",
+ "joint_degree_graph",
+ "directed_joint_degree_graph",
+]
+
+
+@nx._dispatchable(graphs=None)
+def is_valid_joint_degree(joint_degrees):
+ """Checks whether the given joint degree dictionary is realizable.
+
+ A *joint degree dictionary* is a dictionary of dictionaries, in
+ which entry ``joint_degrees[k][l]`` is an integer representing the
+ number of edges joining nodes of degree *k* with nodes of degree
+ *l*. Such a dictionary is realizable as a simple graph if and only
+ if the following conditions are satisfied.
+
+ - each entry must be an integer,
+ - the total number of nodes of degree *k*, computed by
+ ``sum(joint_degrees[k].values()) / k``, must be an integer,
+ - the total number of edges joining nodes of degree *k* with
+ nodes of degree *l* cannot exceed the total number of possible edges,
+ - each diagonal entry ``joint_degrees[k][k]`` must be even (this is
+ a convention assumed by the :func:`joint_degree_graph` function).
+
+
+ Parameters
+ ----------
+ joint_degrees : dictionary of dictionary of integers
+ A joint degree dictionary in which entry ``joint_degrees[k][l]``
+ is the number of edges joining nodes of degree *k* with nodes of
+ degree *l*.
+
+ Returns
+ -------
+ bool
+ Whether the given joint degree dictionary is realizable as a
+ simple graph.
+
+ References
+ ----------
+ .. [1] M. Gjoka, M. Kurant, A. Markopoulou, "2.5K Graphs: from Sampling
+ to Generation", IEEE Infocom, 2013.
+ .. [2] I. Stanton, A. Pinar, "Constructing and sampling graphs with a
+ prescribed joint degree distribution", Journal of Experimental
+ Algorithmics, 2012.
+ """
+
+ degree_count = {}
+ for k in joint_degrees:
+ if k > 0:
+ k_size = sum(joint_degrees[k].values()) / k
+ if not k_size.is_integer():
+ return False
+ degree_count[k] = k_size
+
+ for k in joint_degrees:
+ for l in joint_degrees[k]:
+ if not float(joint_degrees[k][l]).is_integer():
+ return False
+
+ if (k != l) and (joint_degrees[k][l] > degree_count[k] * degree_count[l]):
+ return False
+ elif k == l:
+ if joint_degrees[k][k] > degree_count[k] * (degree_count[k] - 1):
+ return False
+ if joint_degrees[k][k] % 2 != 0:
+ return False
+
+ # if all above conditions have been satisfied then the input
+ # joint degree is realizable as a simple graph.
+ return True
+
+
+def _neighbor_switch(G, w, unsat, h_node_residual, avoid_node_id=None):
+ """Releases one free stub for ``w``, while preserving joint degree in G.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Graph in which the neighbor switch will take place.
+ w : integer
+ Node id for which we will execute this neighbor switch.
+ unsat : set of integers
+ Set of unsaturated node ids that have the same degree as w.
+ h_node_residual: dictionary of integers
+ Keeps track of the remaining stubs for a given node.
+ avoid_node_id: integer
+ Node id to avoid when selecting w_prime.
+
+ Notes
+ -----
+ First, it selects *w_prime*, an unsaturated node that has the same degree
+ as ``w``. Second, it selects *switch_node*, a neighbor node of ``w`` that
+ is not connected to *w_prime*. Then it executes an edge swap i.e. removes
+ (``w``,*switch_node*) and adds (*w_prime*,*switch_node*). Gjoka et. al. [1]
+ prove that such an edge swap is always possible.
+
+ References
+ ----------
+ .. [1] M. Gjoka, B. Tillman, A. Markopoulou, "Construction of Simple
+ Graphs with a Target Joint Degree Matrix and Beyond", IEEE Infocom, '15
+ """
+
+ if (avoid_node_id is None) or (h_node_residual[avoid_node_id] > 1):
+ # select unsaturated node w_prime that has the same degree as w
+ w_prime = next(iter(unsat))
+ else:
+ # assume that the node pair (v,w) has been selected for connection. if
+ # - neighbor_switch is called for node w,
+ # - nodes v and w have the same degree,
+ # - node v=avoid_node_id has only one stub left,
+ # then prevent v=avoid_node_id from being selected as w_prime.
+
+ iter_var = iter(unsat)
+ while True:
+ w_prime = next(iter_var)
+ if w_prime != avoid_node_id:
+ break
+
+ # select switch_node, a neighbor of w, that is not connected to w_prime
+ w_prime_neighbs = G[w_prime] # slightly faster declaring this variable
+ for v in G[w]:
+ if (v not in w_prime_neighbs) and (v != w_prime):
+ switch_node = v
+ break
+
+ # remove edge (w,switch_node), add edge (w_prime,switch_node) and update
+ # data structures
+ G.remove_edge(w, switch_node)
+ G.add_edge(w_prime, switch_node)
+ h_node_residual[w] += 1
+ h_node_residual[w_prime] -= 1
+ if h_node_residual[w_prime] == 0:
+ unsat.remove(w_prime)
+
+
+@py_random_state(1)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def joint_degree_graph(joint_degrees, seed=None):
+ """Generates a random simple graph with the given joint degree dictionary.
+
+ Parameters
+ ----------
+ joint_degrees : dictionary of dictionary of integers
+ A joint degree dictionary in which entry ``joint_degrees[k][l]`` is the
+ number of edges joining nodes of degree *k* with nodes of degree *l*.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : Graph
+ A graph with the specified joint degree dictionary.
+
+ Raises
+ ------
+ NetworkXError
+ If *joint_degrees* dictionary is not realizable.
+
+ Notes
+ -----
+ In each iteration of the "while loop" the algorithm picks two disconnected
+ nodes *v* and *w*, of degree *k* and *l* correspondingly, for which
+ ``joint_degrees[k][l]`` has not reached its target yet. It then adds
+ edge (*v*, *w*) and increases the number of edges in graph G by one.
+
+ The intelligence of the algorithm lies in the fact that it is always
+ possible to add an edge between such disconnected nodes *v* and *w*,
+ even if one or both nodes do not have free stubs. That is made possible by
+ executing a "neighbor switch", an edge rewiring move that releases
+ a free stub while keeping the joint degree of G the same.
+
+ The algorithm continues for E (number of edges) iterations of
+ the "while loop", at the which point all entries of the given
+ ``joint_degrees[k][l]`` have reached their target values and the
+ construction is complete.
+
+ References
+ ----------
+ .. [1] M. Gjoka, B. Tillman, A. Markopoulou, "Construction of Simple
+ Graphs with a Target Joint Degree Matrix and Beyond", IEEE Infocom, '15
+
+ Examples
+ --------
+ >>> joint_degrees = {
+ ... 1: {4: 1},
+ ... 2: {2: 2, 3: 2, 4: 2},
+ ... 3: {2: 2, 4: 1},
+ ... 4: {1: 1, 2: 2, 3: 1},
+ ... }
+ >>> G = nx.joint_degree_graph(joint_degrees)
+ >>>
+ """
+
+ if not is_valid_joint_degree(joint_degrees):
+ msg = "Input joint degree dict not realizable as a simple graph"
+ raise nx.NetworkXError(msg)
+
+ # compute degree count from joint_degrees
+ degree_count = {k: sum(l.values()) // k for k, l in joint_degrees.items() if k > 0}
+
+ # start with empty N-node graph
+ N = sum(degree_count.values())
+ G = nx.empty_graph(N)
+
+ # for a given degree group, keep the list of all node ids
+ h_degree_nodelist = {}
+
+ # for a given node, keep track of the remaining stubs
+ h_node_residual = {}
+
+ # populate h_degree_nodelist and h_node_residual
+ nodeid = 0
+ for degree, num_nodes in degree_count.items():
+ h_degree_nodelist[degree] = range(nodeid, nodeid + num_nodes)
+ for v in h_degree_nodelist[degree]:
+ h_node_residual[v] = degree
+ nodeid += int(num_nodes)
+
+ # iterate over every degree pair (k,l) and add the number of edges given
+ # for each pair
+ for k in joint_degrees:
+ for l in joint_degrees[k]:
+ # n_edges_add is the number of edges to add for the
+ # degree pair (k,l)
+ n_edges_add = joint_degrees[k][l]
+
+ if (n_edges_add > 0) and (k >= l):
+ # number of nodes with degree k and l
+ k_size = degree_count[k]
+ l_size = degree_count[l]
+
+ # k_nodes and l_nodes consist of all nodes of degree k and l
+ k_nodes = h_degree_nodelist[k]
+ l_nodes = h_degree_nodelist[l]
+
+ # k_unsat and l_unsat consist of nodes of degree k and l that
+ # are unsaturated (nodes that have at least 1 available stub)
+ k_unsat = {v for v in k_nodes if h_node_residual[v] > 0}
+
+ if k != l:
+ l_unsat = {w for w in l_nodes if h_node_residual[w] > 0}
+ else:
+ l_unsat = k_unsat
+ n_edges_add = joint_degrees[k][l] // 2
+
+ while n_edges_add > 0:
+ # randomly pick nodes v and w that have degrees k and l
+ v = k_nodes[seed.randrange(k_size)]
+ w = l_nodes[seed.randrange(l_size)]
+
+ # if nodes v and w are disconnected then attempt to connect
+ if not G.has_edge(v, w) and (v != w):
+ # if node v has no free stubs then do neighbor switch
+ if h_node_residual[v] == 0:
+ _neighbor_switch(G, v, k_unsat, h_node_residual)
+
+ # if node w has no free stubs then do neighbor switch
+ if h_node_residual[w] == 0:
+ if k != l:
+ _neighbor_switch(G, w, l_unsat, h_node_residual)
+ else:
+ _neighbor_switch(
+ G, w, l_unsat, h_node_residual, avoid_node_id=v
+ )
+
+ # add edge (v, w) and update data structures
+ G.add_edge(v, w)
+ h_node_residual[v] -= 1
+ h_node_residual[w] -= 1
+ n_edges_add -= 1
+
+ if h_node_residual[v] == 0:
+ k_unsat.discard(v)
+ if h_node_residual[w] == 0:
+ l_unsat.discard(w)
+ return G
+
+
+@nx._dispatchable(graphs=None)
+def is_valid_directed_joint_degree(in_degrees, out_degrees, nkk):
+ """Checks whether the given directed joint degree input is realizable
+
+ Parameters
+ ----------
+ in_degrees : list of integers
+ in degree sequence contains the in degrees of nodes.
+ out_degrees : list of integers
+ out degree sequence contains the out degrees of nodes.
+ nkk : dictionary of dictionary of integers
+ directed joint degree dictionary. for nodes of out degree k (first
+ level of dict) and nodes of in degree l (second level of dict)
+ describes the number of edges.
+
+ Returns
+ -------
+ boolean
+ returns true if given input is realizable, else returns false.
+
+ Notes
+ -----
+ Here is the list of conditions that the inputs (in/out degree sequences,
+ nkk) need to satisfy for simple directed graph realizability:
+
+ - Condition 0: in_degrees and out_degrees have the same length
+ - Condition 1: nkk[k][l] is integer for all k,l
+ - Condition 2: sum(nkk[k])/k = number of nodes with partition id k, is an
+ integer and matching degree sequence
+ - Condition 3: number of edges and non-chords between k and l cannot exceed
+ maximum possible number of edges
+
+
+ References
+ ----------
+ [1] B. Tillman, A. Markopoulou, C. T. Butts & M. Gjoka,
+ "Construction of Directed 2K Graphs". In Proc. of KDD 2017.
+ """
+ V = {} # number of nodes with in/out degree.
+ forbidden = {}
+ if len(in_degrees) != len(out_degrees):
+ return False
+
+ for idx in range(len(in_degrees)):
+ i = in_degrees[idx]
+ o = out_degrees[idx]
+ V[(i, 0)] = V.get((i, 0), 0) + 1
+ V[(o, 1)] = V.get((o, 1), 0) + 1
+
+ forbidden[(o, i)] = forbidden.get((o, i), 0) + 1
+
+ S = {} # number of edges going from in/out degree nodes.
+ for k in nkk:
+ for l in nkk[k]:
+ val = nkk[k][l]
+ if not float(val).is_integer(): # condition 1
+ return False
+
+ if val > 0:
+ S[(k, 1)] = S.get((k, 1), 0) + val
+ S[(l, 0)] = S.get((l, 0), 0) + val
+ # condition 3
+ if val + forbidden.get((k, l), 0) > V[(k, 1)] * V[(l, 0)]:
+ return False
+
+ return all(S[s] / s[0] == V[s] for s in S)
+
+
+def _directed_neighbor_switch(
+ G, w, unsat, h_node_residual_out, chords, h_partition_in, partition
+):
+ """Releases one free stub for node w, while preserving joint degree in G.
+
+ Parameters
+ ----------
+ G : networkx directed graph
+ graph within which the edge swap will take place.
+ w : integer
+ node id for which we need to perform a neighbor switch.
+ unsat: set of integers
+ set of node ids that have the same degree as w and are unsaturated.
+ h_node_residual_out: dict of integers
+ for a given node, keeps track of the remaining stubs to be added.
+ chords: set of tuples
+ keeps track of available positions to add edges.
+ h_partition_in: dict of integers
+ for a given node, keeps track of its partition id (in degree).
+ partition: integer
+ partition id to check if chords have to be updated.
+
+ Notes
+ -----
+ First, it selects node w_prime that (1) has the same degree as w and
+ (2) is unsaturated. Then, it selects node v, a neighbor of w, that is
+ not connected to w_prime and does an edge swap i.e. removes (w,v) and
+ adds (w_prime,v). If neighbor switch is not possible for w using
+ w_prime and v, then return w_prime; in [1] it's proven that
+ such unsaturated nodes can be used.
+
+ References
+ ----------
+ [1] B. Tillman, A. Markopoulou, C. T. Butts & M. Gjoka,
+ "Construction of Directed 2K Graphs". In Proc. of KDD 2017.
+ """
+ w_prime = unsat.pop()
+ unsat.add(w_prime)
+ # select node t, a neighbor of w, that is not connected to w_prime
+ w_neighbs = list(G.successors(w))
+ # slightly faster declaring this variable
+ w_prime_neighbs = list(G.successors(w_prime))
+
+ for v in w_neighbs:
+ if (v not in w_prime_neighbs) and w_prime != v:
+ # removes (w,v), add (w_prime,v) and update data structures
+ G.remove_edge(w, v)
+ G.add_edge(w_prime, v)
+
+ if h_partition_in[v] == partition:
+ chords.add((w, v))
+ chords.discard((w_prime, v))
+
+ h_node_residual_out[w] += 1
+ h_node_residual_out[w_prime] -= 1
+ if h_node_residual_out[w_prime] == 0:
+ unsat.remove(w_prime)
+ return None
+
+ # If neighbor switch didn't work, use unsaturated node
+ return w_prime
+
+
+def _directed_neighbor_switch_rev(
+ G, w, unsat, h_node_residual_in, chords, h_partition_out, partition
+):
+ """The reverse of directed_neighbor_switch.
+
+ Parameters
+ ----------
+ G : networkx directed graph
+ graph within which the edge swap will take place.
+ w : integer
+ node id for which we need to perform a neighbor switch.
+ unsat: set of integers
+ set of node ids that have the same degree as w and are unsaturated.
+ h_node_residual_in: dict of integers
+ for a given node, keeps track of the remaining stubs to be added.
+ chords: set of tuples
+ keeps track of available positions to add edges.
+ h_partition_out: dict of integers
+ for a given node, keeps track of its partition id (out degree).
+ partition: integer
+ partition id to check if chords have to be updated.
+
+ Notes
+ -----
+ Same operation as directed_neighbor_switch except it handles this operation
+ for incoming edges instead of outgoing.
+ """
+ w_prime = unsat.pop()
+ unsat.add(w_prime)
+ # slightly faster declaring these as variables.
+ w_neighbs = list(G.predecessors(w))
+ w_prime_neighbs = list(G.predecessors(w_prime))
+ # select node v, a neighbor of w, that is not connected to w_prime.
+ for v in w_neighbs:
+ if (v not in w_prime_neighbs) and w_prime != v:
+ # removes (v,w), add (v,w_prime) and update data structures.
+ G.remove_edge(v, w)
+ G.add_edge(v, w_prime)
+ if h_partition_out[v] == partition:
+ chords.add((v, w))
+ chords.discard((v, w_prime))
+
+ h_node_residual_in[w] += 1
+ h_node_residual_in[w_prime] -= 1
+ if h_node_residual_in[w_prime] == 0:
+ unsat.remove(w_prime)
+ return None
+
+ # If neighbor switch didn't work, use the unsaturated node.
+ return w_prime
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def directed_joint_degree_graph(in_degrees, out_degrees, nkk, seed=None):
+ """Generates a random simple directed graph with the joint degree.
+
+ Parameters
+ ----------
+ degree_seq : list of tuples (of size 3)
+ degree sequence contains tuples of nodes with node id, in degree and
+ out degree.
+ nkk : dictionary of dictionary of integers
+ directed joint degree dictionary, for nodes of out degree k (first
+ level of dict) and nodes of in degree l (second level of dict)
+ describes the number of edges.
+ seed : hashable object, optional
+ Seed for random number generator.
+
+ Returns
+ -------
+ G : Graph
+ A directed graph with the specified inputs.
+
+ Raises
+ ------
+ NetworkXError
+ If degree_seq and nkk are not realizable as a simple directed graph.
+
+
+ Notes
+ -----
+ Similarly to the undirected version:
+ In each iteration of the "while loop" the algorithm picks two disconnected
+ nodes v and w, of degree k and l correspondingly, for which nkk[k][l] has
+ not reached its target yet i.e. (for given k,l): n_edges_add < nkk[k][l].
+ It then adds edge (v,w) and always increases the number of edges in graph G
+ by one.
+
+ The intelligence of the algorithm lies in the fact that it is always
+ possible to add an edge between disconnected nodes v and w, for which
+ nkk[degree(v)][degree(w)] has not reached its target, even if one or both
+ nodes do not have free stubs. If either node v or w does not have a free
+ stub, we perform a "neighbor switch", an edge rewiring move that releases a
+ free stub while keeping nkk the same.
+
+ The difference for the directed version lies in the fact that neighbor
+ switches might not be able to rewire, but in these cases unsaturated nodes
+ can be reassigned to use instead, see [1] for detailed description and
+ proofs.
+
+ The algorithm continues for E (number of edges in the graph) iterations of
+ the "while loop", at which point all entries of the given nkk[k][l] have
+ reached their target values and the construction is complete.
+
+ References
+ ----------
+ [1] B. Tillman, A. Markopoulou, C. T. Butts & M. Gjoka,
+ "Construction of Directed 2K Graphs". In Proc. of KDD 2017.
+
+ Examples
+ --------
+ >>> in_degrees = [0, 1, 1, 2]
+ >>> out_degrees = [1, 1, 1, 1]
+ >>> nkk = {1: {1: 2, 2: 2}}
+ >>> G = nx.directed_joint_degree_graph(in_degrees, out_degrees, nkk)
+ >>>
+ """
+ if not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk):
+ msg = "Input is not realizable as a simple graph"
+ raise nx.NetworkXError(msg)
+
+ # start with an empty directed graph.
+ G = nx.DiGraph()
+
+ # for a given group, keep the list of all node ids.
+ h_degree_nodelist_in = {}
+ h_degree_nodelist_out = {}
+ # for a given group, keep the list of all unsaturated node ids.
+ h_degree_nodelist_in_unsat = {}
+ h_degree_nodelist_out_unsat = {}
+ # for a given node, keep track of the remaining stubs to be added.
+ h_node_residual_out = {}
+ h_node_residual_in = {}
+ # for a given node, keep track of the partition id.
+ h_partition_out = {}
+ h_partition_in = {}
+ # keep track of non-chords between pairs of partition ids.
+ non_chords = {}
+
+ # populate data structures
+ for idx, i in enumerate(in_degrees):
+ idx = int(idx)
+ if i > 0:
+ h_degree_nodelist_in.setdefault(i, [])
+ h_degree_nodelist_in_unsat.setdefault(i, set())
+ h_degree_nodelist_in[i].append(idx)
+ h_degree_nodelist_in_unsat[i].add(idx)
+ h_node_residual_in[idx] = i
+ h_partition_in[idx] = i
+
+ for idx, o in enumerate(out_degrees):
+ o = out_degrees[idx]
+ non_chords[(o, in_degrees[idx])] = non_chords.get((o, in_degrees[idx]), 0) + 1
+ idx = int(idx)
+ if o > 0:
+ h_degree_nodelist_out.setdefault(o, [])
+ h_degree_nodelist_out_unsat.setdefault(o, set())
+ h_degree_nodelist_out[o].append(idx)
+ h_degree_nodelist_out_unsat[o].add(idx)
+ h_node_residual_out[idx] = o
+ h_partition_out[idx] = o
+
+ G.add_node(idx)
+
+ nk_in = {}
+ nk_out = {}
+ for p in h_degree_nodelist_in:
+ nk_in[p] = len(h_degree_nodelist_in[p])
+ for p in h_degree_nodelist_out:
+ nk_out[p] = len(h_degree_nodelist_out[p])
+
+ # iterate over every degree pair (k,l) and add the number of edges given
+ # for each pair.
+ for k in nkk:
+ for l in nkk[k]:
+ n_edges_add = nkk[k][l]
+
+ if n_edges_add > 0:
+ # chords contains a random set of potential edges.
+ chords = set()
+
+ k_len = nk_out[k]
+ l_len = nk_in[l]
+ chords_sample = seed.sample(
+ range(k_len * l_len), n_edges_add + non_chords.get((k, l), 0)
+ )
+
+ num = 0
+ while len(chords) < n_edges_add:
+ i = h_degree_nodelist_out[k][chords_sample[num] % k_len]
+ j = h_degree_nodelist_in[l][chords_sample[num] // k_len]
+ num += 1
+ if i != j:
+ chords.add((i, j))
+
+ # k_unsat and l_unsat consist of nodes of in/out degree k and l
+ # that are unsaturated i.e. those nodes that have at least one
+ # available stub
+ k_unsat = h_degree_nodelist_out_unsat[k]
+ l_unsat = h_degree_nodelist_in_unsat[l]
+
+ while n_edges_add > 0:
+ v, w = chords.pop()
+ chords.add((v, w))
+
+ # if node v has no free stubs then do neighbor switch.
+ if h_node_residual_out[v] == 0:
+ _v = _directed_neighbor_switch(
+ G,
+ v,
+ k_unsat,
+ h_node_residual_out,
+ chords,
+ h_partition_in,
+ l,
+ )
+ if _v is not None:
+ v = _v
+
+ # if node w has no free stubs then do neighbor switch.
+ if h_node_residual_in[w] == 0:
+ _w = _directed_neighbor_switch_rev(
+ G,
+ w,
+ l_unsat,
+ h_node_residual_in,
+ chords,
+ h_partition_out,
+ k,
+ )
+ if _w is not None:
+ w = _w
+
+ # add edge (v,w) and update data structures.
+ G.add_edge(v, w)
+ h_node_residual_out[v] -= 1
+ h_node_residual_in[w] -= 1
+ n_edges_add -= 1
+ chords.discard((v, w))
+
+ if h_node_residual_out[v] == 0:
+ k_unsat.discard(v)
+ if h_node_residual_in[w] == 0:
+ l_unsat.discard(w)
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/lattice.py b/.venv/lib/python3.12/site-packages/networkx/generators/lattice.py
new file mode 100644
index 00000000..95e520d2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/lattice.py
@@ -0,0 +1,367 @@
+"""Functions for generating grid graphs and lattices
+
+The :func:`grid_2d_graph`, :func:`triangular_lattice_graph`, and
+:func:`hexagonal_lattice_graph` functions correspond to the three
+`regular tilings of the plane`_, the square, triangular, and hexagonal
+tilings, respectively. :func:`grid_graph` and :func:`hypercube_graph`
+are similar for arbitrary dimensions. Useful relevant discussion can
+be found about `Triangular Tiling`_, and `Square, Hex and Triangle Grids`_
+
+.. _regular tilings of the plane: https://en.wikipedia.org/wiki/List_of_regular_polytopes_and_compounds#Euclidean_tilings
+.. _Square, Hex and Triangle Grids: http://www-cs-students.stanford.edu/~amitp/game-programming/grids/
+.. _Triangular Tiling: https://en.wikipedia.org/wiki/Triangular_tiling
+
+"""
+
+from itertools import repeat
+from math import sqrt
+
+import networkx as nx
+from networkx.classes import set_node_attributes
+from networkx.exception import NetworkXError
+from networkx.generators.classic import cycle_graph, empty_graph, path_graph
+from networkx.relabel import relabel_nodes
+from networkx.utils import flatten, nodes_or_number, pairwise
+
+__all__ = [
+ "grid_2d_graph",
+ "grid_graph",
+ "hypercube_graph",
+ "triangular_lattice_graph",
+ "hexagonal_lattice_graph",
+]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+@nodes_or_number([0, 1])
+def grid_2d_graph(m, n, periodic=False, create_using=None):
+ """Returns the two-dimensional grid graph.
+
+ The grid graph has each node connected to its four nearest neighbors.
+
+ Parameters
+ ----------
+ m, n : int or iterable container of nodes
+ If an integer, nodes are from `range(n)`.
+ If a container, elements become the coordinate of the nodes.
+
+ periodic : bool or iterable
+ If `periodic` is True, both dimensions are periodic. If False, none
+ are periodic. If `periodic` is iterable, it should yield 2 bool
+ values indicating whether the 1st and 2nd axes, respectively, are
+ periodic.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ NetworkX graph
+ The (possibly periodic) grid graph of the specified dimensions.
+
+ """
+ G = empty_graph(0, create_using)
+ row_name, rows = m
+ col_name, cols = n
+ G.add_nodes_from((i, j) for i in rows for j in cols)
+ G.add_edges_from(((i, j), (pi, j)) for pi, i in pairwise(rows) for j in cols)
+ G.add_edges_from(((i, j), (i, pj)) for i in rows for pj, j in pairwise(cols))
+
+ try:
+ periodic_r, periodic_c = periodic
+ except TypeError:
+ periodic_r = periodic_c = periodic
+
+ if periodic_r and len(rows) > 2:
+ first = rows[0]
+ last = rows[-1]
+ G.add_edges_from(((first, j), (last, j)) for j in cols)
+ if periodic_c and len(cols) > 2:
+ first = cols[0]
+ last = cols[-1]
+ G.add_edges_from(((i, first), (i, last)) for i in rows)
+ # both directions for directed
+ if G.is_directed():
+ G.add_edges_from((v, u) for u, v in G.edges())
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def grid_graph(dim, periodic=False):
+ """Returns the *n*-dimensional grid graph.
+
+ The dimension *n* is the length of the list `dim` and the size in
+ each dimension is the value of the corresponding list element.
+
+ Parameters
+ ----------
+ dim : list or tuple of numbers or iterables of nodes
+ 'dim' is a tuple or list with, for each dimension, either a number
+ that is the size of that dimension or an iterable of nodes for
+ that dimension. The dimension of the grid_graph is the length
+ of `dim`.
+
+ periodic : bool or iterable
+ If `periodic` is True, all dimensions are periodic. If False all
+ dimensions are not periodic. If `periodic` is iterable, it should
+ yield `dim` bool values each of which indicates whether the
+ corresponding axis is periodic.
+
+ Returns
+ -------
+ NetworkX graph
+ The (possibly periodic) grid graph of the specified dimensions.
+
+ Examples
+ --------
+ To produce a 2 by 3 by 4 grid graph, a graph on 24 nodes:
+
+ >>> from networkx import grid_graph
+ >>> G = grid_graph(dim=(2, 3, 4))
+ >>> len(G)
+ 24
+ >>> G = grid_graph(dim=(range(7, 9), range(3, 6)))
+ >>> len(G)
+ 6
+ """
+ from networkx.algorithms.operators.product import cartesian_product
+
+ if not dim:
+ return empty_graph(0)
+
+ try:
+ func = (cycle_graph if p else path_graph for p in periodic)
+ except TypeError:
+ func = repeat(cycle_graph if periodic else path_graph)
+
+ G = next(func)(dim[0])
+ for current_dim in dim[1:]:
+ Gnew = next(func)(current_dim)
+ G = cartesian_product(Gnew, G)
+ # graph G is done but has labels of the form (1, (2, (3, 1))) so relabel
+ H = relabel_nodes(G, flatten)
+ return H
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def hypercube_graph(n):
+ """Returns the *n*-dimensional hypercube graph.
+
+ The nodes are the integers between 0 and ``2 ** n - 1``, inclusive.
+
+ For more information on the hypercube graph, see the Wikipedia
+ article `Hypercube graph`_.
+
+ .. _Hypercube graph: https://en.wikipedia.org/wiki/Hypercube_graph
+
+ Parameters
+ ----------
+ n : int
+ The dimension of the hypercube.
+ The number of nodes in the graph will be ``2 ** n``.
+
+ Returns
+ -------
+ NetworkX graph
+ The hypercube graph of dimension *n*.
+ """
+ dim = n * [2]
+ G = grid_graph(dim)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def triangular_lattice_graph(
+ m, n, periodic=False, with_positions=True, create_using=None
+):
+ r"""Returns the $m$ by $n$ triangular lattice graph.
+
+ The `triangular lattice graph`_ is a two-dimensional `grid graph`_ in
+ which each square unit has a diagonal edge (each grid unit has a chord).
+
+ The returned graph has $m$ rows and $n$ columns of triangles. Rows and
+ columns include both triangles pointing up and down. Rows form a strip
+ of constant height. Columns form a series of diamond shapes, staggered
+ with the columns on either side. Another way to state the size is that
+ the nodes form a grid of `m+1` rows and `(n + 1) // 2` columns.
+ The odd row nodes are shifted horizontally relative to the even rows.
+
+ Directed graph types have edges pointed up or right.
+
+ Positions of nodes are computed by default or `with_positions is True`.
+ The position of each node (embedded in a euclidean plane) is stored in
+ the graph using equilateral triangles with sidelength 1.
+ The height between rows of nodes is thus $\sqrt(3)/2$.
+ Nodes lie in the first quadrant with the node $(0, 0)$ at the origin.
+
+ .. _triangular lattice graph: http://mathworld.wolfram.com/TriangularGrid.html
+ .. _grid graph: http://www-cs-students.stanford.edu/~amitp/game-programming/grids/
+ .. _Triangular Tiling: https://en.wikipedia.org/wiki/Triangular_tiling
+
+ Parameters
+ ----------
+ m : int
+ The number of rows in the lattice.
+
+ n : int
+ The number of columns in the lattice.
+
+ periodic : bool (default: False)
+ If True, join the boundary vertices of the grid using periodic
+ boundary conditions. The join between boundaries is the final row
+ and column of triangles. This means there is one row and one column
+ fewer nodes for the periodic lattice. Periodic lattices require
+ `m >= 3`, `n >= 5` and are allowed but misaligned if `m` or `n` are odd
+
+ with_positions : bool (default: True)
+ Store the coordinates of each node in the graph node attribute 'pos'.
+ The coordinates provide a lattice with equilateral triangles.
+ Periodic positions shift the nodes vertically in a nonlinear way so
+ the edges don't overlap so much.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ NetworkX graph
+ The *m* by *n* triangular lattice graph.
+ """
+ H = empty_graph(0, create_using)
+ if n == 0 or m == 0:
+ return H
+ if periodic:
+ if n < 5 or m < 3:
+ msg = f"m > 2 and n > 4 required for periodic. m={m}, n={n}"
+ raise NetworkXError(msg)
+
+ N = (n + 1) // 2 # number of nodes in row
+ rows = range(m + 1)
+ cols = range(N + 1)
+ # Make grid
+ H.add_edges_from(((i, j), (i + 1, j)) for j in rows for i in cols[:N])
+ H.add_edges_from(((i, j), (i, j + 1)) for j in rows[:m] for i in cols)
+ # add diagonals
+ H.add_edges_from(((i, j), (i + 1, j + 1)) for j in rows[1:m:2] for i in cols[:N])
+ H.add_edges_from(((i + 1, j), (i, j + 1)) for j in rows[:m:2] for i in cols[:N])
+ # identify boundary nodes if periodic
+ from networkx.algorithms.minors import contracted_nodes
+
+ if periodic is True:
+ for i in cols:
+ H = contracted_nodes(H, (i, 0), (i, m))
+ for j in rows[:m]:
+ H = contracted_nodes(H, (0, j), (N, j))
+ elif n % 2:
+ # remove extra nodes
+ H.remove_nodes_from((N, j) for j in rows[1::2])
+
+ # Add position node attributes
+ if with_positions:
+ ii = (i for i in cols for j in rows)
+ jj = (j for i in cols for j in rows)
+ xx = (0.5 * (j % 2) + i for i in cols for j in rows)
+ h = sqrt(3) / 2
+ if periodic:
+ yy = (h * j + 0.01 * i * i for i in cols for j in rows)
+ else:
+ yy = (h * j for i in cols for j in rows)
+ pos = {(i, j): (x, y) for i, j, x, y in zip(ii, jj, xx, yy) if (i, j) in H}
+ set_node_attributes(H, pos, "pos")
+ return H
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def hexagonal_lattice_graph(
+ m, n, periodic=False, with_positions=True, create_using=None
+):
+ """Returns an `m` by `n` hexagonal lattice graph.
+
+ The *hexagonal lattice graph* is a graph whose nodes and edges are
+ the `hexagonal tiling`_ of the plane.
+
+ The returned graph will have `m` rows and `n` columns of hexagons.
+ `Odd numbered columns`_ are shifted up relative to even numbered columns.
+
+ Positions of nodes are computed by default or `with_positions is True`.
+ Node positions creating the standard embedding in the plane
+ with sidelength 1 and are stored in the node attribute 'pos'.
+ `pos = nx.get_node_attributes(G, 'pos')` creates a dict ready for drawing.
+
+ .. _hexagonal tiling: https://en.wikipedia.org/wiki/Hexagonal_tiling
+ .. _Odd numbered columns: http://www-cs-students.stanford.edu/~amitp/game-programming/grids/
+
+ Parameters
+ ----------
+ m : int
+ The number of rows of hexagons in the lattice.
+
+ n : int
+ The number of columns of hexagons in the lattice.
+
+ periodic : bool
+ Whether to make a periodic grid by joining the boundary vertices.
+ For this to work `n` must be even and both `n > 1` and `m > 1`.
+ The periodic connections create another row and column of hexagons
+ so these graphs have fewer nodes as boundary nodes are identified.
+
+ with_positions : bool (default: True)
+ Store the coordinates of each node in the graph node attribute 'pos'.
+ The coordinates provide a lattice with vertical columns of hexagons
+ offset to interleave and cover the plane.
+ Periodic positions shift the nodes vertically in a nonlinear way so
+ the edges don't overlap so much.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ If graph is directed, edges will point up or right.
+
+ Returns
+ -------
+ NetworkX graph
+ The *m* by *n* hexagonal lattice graph.
+ """
+ G = empty_graph(0, create_using)
+ if m == 0 or n == 0:
+ return G
+ if periodic and (n % 2 == 1 or m < 2 or n < 2):
+ msg = "periodic hexagonal lattice needs m > 1, n > 1 and even n"
+ raise NetworkXError(msg)
+
+ M = 2 * m # twice as many nodes as hexagons vertically
+ rows = range(M + 2)
+ cols = range(n + 1)
+ # make lattice
+ col_edges = (((i, j), (i, j + 1)) for i in cols for j in rows[: M + 1])
+ row_edges = (((i, j), (i + 1, j)) for i in cols[:n] for j in rows if i % 2 == j % 2)
+ G.add_edges_from(col_edges)
+ G.add_edges_from(row_edges)
+ # Remove corner nodes with one edge
+ G.remove_node((0, M + 1))
+ G.remove_node((n, (M + 1) * (n % 2)))
+
+ # identify boundary nodes if periodic
+ from networkx.algorithms.minors import contracted_nodes
+
+ if periodic:
+ for i in cols[:n]:
+ G = contracted_nodes(G, (i, 0), (i, M))
+ for i in cols[1:]:
+ G = contracted_nodes(G, (i, 1), (i, M + 1))
+ for j in rows[1:M]:
+ G = contracted_nodes(G, (0, j), (n, j))
+ G.remove_node((n, M))
+
+ # calc position in embedded space
+ ii = (i for i in cols for j in rows)
+ jj = (j for i in cols for j in rows)
+ xx = (0.5 + i + i // 2 + (j % 2) * ((i % 2) - 0.5) for i in cols for j in rows)
+ h = sqrt(3) / 2
+ if periodic:
+ yy = (h * j + 0.01 * i * i for i in cols for j in rows)
+ else:
+ yy = (h * j for i in cols for j in rows)
+ # exclude nodes not in G
+ pos = {(i, j): (x, y) for i, j, x, y in zip(ii, jj, xx, yy) if (i, j) in G}
+ set_node_attributes(G, pos, "pos")
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/line.py b/.venv/lib/python3.12/site-packages/networkx/generators/line.py
new file mode 100644
index 00000000..87d25182
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/line.py
@@ -0,0 +1,500 @@
+"""Functions for generating line graphs."""
+
+from collections import defaultdict
+from functools import partial
+from itertools import combinations
+
+import networkx as nx
+from networkx.utils import arbitrary_element
+from networkx.utils.decorators import not_implemented_for
+
+__all__ = ["line_graph", "inverse_line_graph"]
+
+
+@nx._dispatchable(returns_graph=True)
+def line_graph(G, create_using=None):
+ r"""Returns the line graph of the graph or digraph `G`.
+
+ The line graph of a graph `G` has a node for each edge in `G` and an
+ edge joining those nodes if the two edges in `G` share a common node. For
+ directed graphs, nodes are adjacent exactly when the edges they represent
+ form a directed path of length two.
+
+ The nodes of the line graph are 2-tuples of nodes in the original graph (or
+ 3-tuples for multigraphs, with the key of the edge as the third element).
+
+ For information about self-loops and more discussion, see the **Notes**
+ section below.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX Graph, DiGraph, MultiGraph, or MultiDigraph.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ L : graph
+ The line graph of G.
+
+ Examples
+ --------
+ >>> G = nx.star_graph(3)
+ >>> L = nx.line_graph(G)
+ >>> print(sorted(map(sorted, L.edges()))) # makes a 3-clique, K3
+ [[(0, 1), (0, 2)], [(0, 1), (0, 3)], [(0, 2), (0, 3)]]
+
+ Edge attributes from `G` are not copied over as node attributes in `L`, but
+ attributes can be copied manually:
+
+ >>> G = nx.path_graph(4)
+ >>> G.add_edges_from((u, v, {"tot": u + v}) for u, v in G.edges)
+ >>> G.edges(data=True)
+ EdgeDataView([(0, 1, {'tot': 1}), (1, 2, {'tot': 3}), (2, 3, {'tot': 5})])
+ >>> H = nx.line_graph(G)
+ >>> H.add_nodes_from((node, G.edges[node]) for node in H)
+ >>> H.nodes(data=True)
+ NodeDataView({(0, 1): {'tot': 1}, (2, 3): {'tot': 5}, (1, 2): {'tot': 3}})
+
+ Notes
+ -----
+ Graph, node, and edge data are not propagated to the new graph. For
+ undirected graphs, the nodes in G must be sortable, otherwise the
+ constructed line graph may not be correct.
+
+ *Self-loops in undirected graphs*
+
+ For an undirected graph `G` without multiple edges, each edge can be
+ written as a set `\{u, v\}`. Its line graph `L` has the edges of `G` as
+ its nodes. If `x` and `y` are two nodes in `L`, then `\{x, y\}` is an edge
+ in `L` if and only if the intersection of `x` and `y` is nonempty. Thus,
+ the set of all edges is determined by the set of all pairwise intersections
+ of edges in `G`.
+
+ Trivially, every edge in G would have a nonzero intersection with itself,
+ and so every node in `L` should have a self-loop. This is not so
+ interesting, and the original context of line graphs was with simple
+ graphs, which had no self-loops or multiple edges. The line graph was also
+ meant to be a simple graph and thus, self-loops in `L` are not part of the
+ standard definition of a line graph. In a pairwise intersection matrix,
+ this is analogous to excluding the diagonal entries from the line graph
+ definition.
+
+ Self-loops and multiple edges in `G` add nodes to `L` in a natural way, and
+ do not require any fundamental changes to the definition. It might be
+ argued that the self-loops we excluded before should now be included.
+ However, the self-loops are still "trivial" in some sense and thus, are
+ usually excluded.
+
+ *Self-loops in directed graphs*
+
+ For a directed graph `G` without multiple edges, each edge can be written
+ as a tuple `(u, v)`. Its line graph `L` has the edges of `G` as its
+ nodes. If `x` and `y` are two nodes in `L`, then `(x, y)` is an edge in `L`
+ if and only if the tail of `x` matches the head of `y`, for example, if `x
+ = (a, b)` and `y = (b, c)` for some vertices `a`, `b`, and `c` in `G`.
+
+ Due to the directed nature of the edges, it is no longer the case that
+ every edge in `G` should have a self-loop in `L`. Now, the only time
+ self-loops arise is if a node in `G` itself has a self-loop. So such
+ self-loops are no longer "trivial" but instead, represent essential
+ features of the topology of `G`. For this reason, the historical
+ development of line digraphs is such that self-loops are included. When the
+ graph `G` has multiple edges, once again only superficial changes are
+ required to the definition.
+
+ References
+ ----------
+ * Harary, Frank, and Norman, Robert Z., "Some properties of line digraphs",
+ Rend. Circ. Mat. Palermo, II. Ser. 9 (1960), 161--168.
+ * Hemminger, R. L.; Beineke, L. W. (1978), "Line graphs and line digraphs",
+ in Beineke, L. W.; Wilson, R. J., Selected Topics in Graph Theory,
+ Academic Press Inc., pp. 271--305.
+
+ """
+ if G.is_directed():
+ L = _lg_directed(G, create_using=create_using)
+ else:
+ L = _lg_undirected(G, selfloops=False, create_using=create_using)
+ return L
+
+
+def _lg_directed(G, create_using=None):
+ """Returns the line graph L of the (multi)digraph G.
+
+ Edges in G appear as nodes in L, represented as tuples of the form (u,v)
+ or (u,v,key) if G is a multidigraph. A node in L corresponding to the edge
+ (u,v) is connected to every node corresponding to an edge (v,w).
+
+ Parameters
+ ----------
+ G : digraph
+ A directed graph or directed multigraph.
+ create_using : NetworkX graph constructor, optional
+ Graph type to create. If graph instance, then cleared before populated.
+ Default is to use the same graph class as `G`.
+
+ """
+ L = nx.empty_graph(0, create_using, default=G.__class__)
+
+ # Create a graph specific edge function.
+ get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges
+
+ for from_node in get_edges():
+ # from_node is: (u,v) or (u,v,key)
+ L.add_node(from_node)
+ for to_node in get_edges(from_node[1]):
+ L.add_edge(from_node, to_node)
+
+ return L
+
+
+def _lg_undirected(G, selfloops=False, create_using=None):
+ """Returns the line graph L of the (multi)graph G.
+
+ Edges in G appear as nodes in L, represented as sorted tuples of the form
+ (u,v), or (u,v,key) if G is a multigraph. A node in L corresponding to
+ the edge {u,v} is connected to every node corresponding to an edge that
+ involves u or v.
+
+ Parameters
+ ----------
+ G : graph
+ An undirected graph or multigraph.
+ selfloops : bool
+ If `True`, then self-loops are included in the line graph. If `False`,
+ they are excluded.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Notes
+ -----
+ The standard algorithm for line graphs of undirected graphs does not
+ produce self-loops.
+
+ """
+ L = nx.empty_graph(0, create_using, default=G.__class__)
+
+ # Graph specific functions for edges.
+ get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges
+
+ # Determine if we include self-loops or not.
+ shift = 0 if selfloops else 1
+
+ # Introduce numbering of nodes
+ node_index = {n: i for i, n in enumerate(G)}
+
+ # Lift canonical representation of nodes to edges in line graph
+ edge_key_function = lambda edge: (node_index[edge[0]], node_index[edge[1]])
+
+ edges = set()
+ for u in G:
+ # Label nodes as a sorted tuple of nodes in original graph.
+ # Decide on representation of {u, v} as (u, v) or (v, u) depending on node_index.
+ # -> This ensures a canonical representation and avoids comparing values of different types.
+ nodes = [tuple(sorted(x[:2], key=node_index.get)) + x[2:] for x in get_edges(u)]
+
+ if len(nodes) == 1:
+ # Then the edge will be an isolated node in L.
+ L.add_node(nodes[0])
+
+ # Add a clique of `nodes` to graph. To prevent double adding edges,
+ # especially important for multigraphs, we store the edges in
+ # canonical form in a set.
+ for i, a in enumerate(nodes):
+ edges.update(
+ [
+ tuple(sorted((a, b), key=edge_key_function))
+ for b in nodes[i + shift :]
+ ]
+ )
+
+ L.add_edges_from(edges)
+ return L
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(returns_graph=True)
+def inverse_line_graph(G):
+ """Returns the inverse line graph of graph G.
+
+ If H is a graph, and G is the line graph of H, such that G = L(H).
+ Then H is the inverse line graph of G.
+
+ Not all graphs are line graphs and these do not have an inverse line graph.
+ In these cases this function raises a NetworkXError.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX Graph
+
+ Returns
+ -------
+ H : graph
+ The inverse line graph of G.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If G is directed or a multigraph
+
+ NetworkXError
+ If G is not a line graph
+
+ Notes
+ -----
+ This is an implementation of the Roussopoulos algorithm[1]_.
+
+ If G consists of multiple components, then the algorithm doesn't work.
+ You should invert every component separately:
+
+ >>> K5 = nx.complete_graph(5)
+ >>> P4 = nx.Graph([("a", "b"), ("b", "c"), ("c", "d")])
+ >>> G = nx.union(K5, P4)
+ >>> root_graphs = []
+ >>> for comp in nx.connected_components(G):
+ ... root_graphs.append(nx.inverse_line_graph(G.subgraph(comp)))
+ >>> len(root_graphs)
+ 2
+
+ References
+ ----------
+ .. [1] Roussopoulos, N.D. , "A max {m, n} algorithm for determining the graph H from
+ its line graph G", Information Processing Letters 2, (1973), 108--112, ISSN 0020-0190,
+ `DOI link <https://doi.org/10.1016/0020-0190(73)90029-X>`_
+
+ """
+ if G.number_of_nodes() == 0:
+ return nx.empty_graph(1)
+ elif G.number_of_nodes() == 1:
+ v = arbitrary_element(G)
+ a = (v, 0)
+ b = (v, 1)
+ H = nx.Graph([(a, b)])
+ return H
+ elif G.number_of_nodes() > 1 and G.number_of_edges() == 0:
+ msg = (
+ "inverse_line_graph() doesn't work on an edgeless graph. "
+ "Please use this function on each component separately."
+ )
+ raise nx.NetworkXError(msg)
+
+ if nx.number_of_selfloops(G) != 0:
+ msg = (
+ "A line graph as generated by NetworkX has no selfloops, so G has no "
+ "inverse line graph. Please remove the selfloops from G and try again."
+ )
+ raise nx.NetworkXError(msg)
+
+ starting_cell = _select_starting_cell(G)
+ P = _find_partition(G, starting_cell)
+ # count how many times each vertex appears in the partition set
+ P_count = {u: 0 for u in G.nodes}
+ for p in P:
+ for u in p:
+ P_count[u] += 1
+
+ if max(P_count.values()) > 2:
+ msg = "G is not a line graph (vertex found in more than two partition cells)"
+ raise nx.NetworkXError(msg)
+ W = tuple((u,) for u in P_count if P_count[u] == 1)
+ H = nx.Graph()
+ H.add_nodes_from(P)
+ H.add_nodes_from(W)
+ for a, b in combinations(H.nodes, 2):
+ if any(a_bit in b for a_bit in a):
+ H.add_edge(a, b)
+ return H
+
+
+def _triangles(G, e):
+ """Return list of all triangles containing edge e"""
+ u, v = e
+ if u not in G:
+ raise nx.NetworkXError(f"Vertex {u} not in graph")
+ if v not in G[u]:
+ raise nx.NetworkXError(f"Edge ({u}, {v}) not in graph")
+ triangle_list = []
+ for x in G[u]:
+ if x in G[v]:
+ triangle_list.append((u, v, x))
+ return triangle_list
+
+
+def _odd_triangle(G, T):
+ """Test whether T is an odd triangle in G
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+ T : 3-tuple of vertices forming triangle in G
+
+ Returns
+ -------
+ True is T is an odd triangle
+ False otherwise
+
+ Raises
+ ------
+ NetworkXError
+ T is not a triangle in G
+
+ Notes
+ -----
+ An odd triangle is one in which there exists another vertex in G which is
+ adjacent to either exactly one or exactly all three of the vertices in the
+ triangle.
+
+ """
+ for u in T:
+ if u not in G.nodes():
+ raise nx.NetworkXError(f"Vertex {u} not in graph")
+ for e in list(combinations(T, 2)):
+ if e[0] not in G[e[1]]:
+ raise nx.NetworkXError(f"Edge ({e[0]}, {e[1]}) not in graph")
+
+ T_nbrs = defaultdict(int)
+ for t in T:
+ for v in G[t]:
+ if v not in T:
+ T_nbrs[v] += 1
+ return any(T_nbrs[v] in [1, 3] for v in T_nbrs)
+
+
+def _find_partition(G, starting_cell):
+ """Find a partition of the vertices of G into cells of complete graphs
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+ starting_cell : tuple of vertices in G which form a cell
+
+ Returns
+ -------
+ List of tuples of vertices of G
+
+ Raises
+ ------
+ NetworkXError
+ If a cell is not a complete subgraph then G is not a line graph
+ """
+ G_partition = G.copy()
+ P = [starting_cell] # partition set
+ G_partition.remove_edges_from(list(combinations(starting_cell, 2)))
+ # keep list of partitioned nodes which might have an edge in G_partition
+ partitioned_vertices = list(starting_cell)
+ while G_partition.number_of_edges() > 0:
+ # there are still edges left and so more cells to be made
+ u = partitioned_vertices.pop()
+ deg_u = len(G_partition[u])
+ if deg_u != 0:
+ # if u still has edges then we need to find its other cell
+ # this other cell must be a complete subgraph or else G is
+ # not a line graph
+ new_cell = [u] + list(G_partition[u])
+ for u in new_cell:
+ for v in new_cell:
+ if (u != v) and (v not in G_partition[u]):
+ msg = (
+ "G is not a line graph "
+ "(partition cell not a complete subgraph)"
+ )
+ raise nx.NetworkXError(msg)
+ P.append(tuple(new_cell))
+ G_partition.remove_edges_from(list(combinations(new_cell, 2)))
+ partitioned_vertices += new_cell
+ return P
+
+
+def _select_starting_cell(G, starting_edge=None):
+ """Select a cell to initiate _find_partition
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+ starting_edge: an edge to build the starting cell from
+
+ Returns
+ -------
+ Tuple of vertices in G
+
+ Raises
+ ------
+ NetworkXError
+ If it is determined that G is not a line graph
+
+ Notes
+ -----
+ If starting edge not specified then pick an arbitrary edge - doesn't
+ matter which. However, this function may call itself requiring a
+ specific starting edge. Note that the r, s notation for counting
+ triangles is the same as in the Roussopoulos paper cited above.
+ """
+ if starting_edge is None:
+ e = arbitrary_element(G.edges())
+ else:
+ e = starting_edge
+ if e[0] not in G.nodes():
+ raise nx.NetworkXError(f"Vertex {e[0]} not in graph")
+ if e[1] not in G[e[0]]:
+ msg = f"starting_edge ({e[0]}, {e[1]}) is not in the Graph"
+ raise nx.NetworkXError(msg)
+ e_triangles = _triangles(G, e)
+ r = len(e_triangles)
+ if r == 0:
+ # there are no triangles containing e, so the starting cell is just e
+ starting_cell = e
+ elif r == 1:
+ # there is exactly one triangle, T, containing e. If other 2 edges
+ # of T belong only to this triangle then T is starting cell
+ T = e_triangles[0]
+ a, b, c = T
+ # ab was original edge so check the other 2 edges
+ ac_edges = len(_triangles(G, (a, c)))
+ bc_edges = len(_triangles(G, (b, c)))
+ if ac_edges == 1:
+ if bc_edges == 1:
+ starting_cell = T
+ else:
+ return _select_starting_cell(G, starting_edge=(b, c))
+ else:
+ return _select_starting_cell(G, starting_edge=(a, c))
+ else:
+ # r >= 2 so we need to count the number of odd triangles, s
+ s = 0
+ odd_triangles = []
+ for T in e_triangles:
+ if _odd_triangle(G, T):
+ s += 1
+ odd_triangles.append(T)
+ if r == 2 and s == 0:
+ # in this case either triangle works, so just use T
+ starting_cell = T
+ elif r - 1 <= s <= r:
+ # check if odd triangles containing e form complete subgraph
+ triangle_nodes = set()
+ for T in odd_triangles:
+ for x in T:
+ triangle_nodes.add(x)
+
+ for u in triangle_nodes:
+ for v in triangle_nodes:
+ if u != v and (v not in G[u]):
+ msg = (
+ "G is not a line graph (odd triangles "
+ "do not form complete subgraph)"
+ )
+ raise nx.NetworkXError(msg)
+ # otherwise then we can use this as the starting cell
+ starting_cell = tuple(triangle_nodes)
+
+ else:
+ msg = (
+ "G is not a line graph (incorrect number of "
+ "odd triangles around starting edge)"
+ )
+ raise nx.NetworkXError(msg)
+ return starting_cell
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/mycielski.py b/.venv/lib/python3.12/site-packages/networkx/generators/mycielski.py
new file mode 100644
index 00000000..804b9036
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/mycielski.py
@@ -0,0 +1,110 @@
+"""Functions related to the Mycielski Operation and the Mycielskian family
+of graphs.
+
+"""
+
+import networkx as nx
+from networkx.utils import not_implemented_for
+
+__all__ = ["mycielskian", "mycielski_graph"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(returns_graph=True)
+def mycielskian(G, iterations=1):
+ r"""Returns the Mycielskian of a simple, undirected graph G
+
+ The Mycielskian of graph preserves a graph's triangle free
+ property while increasing the chromatic number by 1.
+
+ The Mycielski Operation on a graph, :math:`G=(V, E)`, constructs a new
+ graph with :math:`2|V| + 1` nodes and :math:`3|E| + |V|` edges.
+
+ The construction is as follows:
+
+ Let :math:`V = {0, ..., n-1}`. Construct another vertex set
+ :math:`U = {n, ..., 2n}` and a vertex, `w`.
+ Construct a new graph, `M`, with vertices :math:`U \bigcup V \bigcup w`.
+ For edges, :math:`(u, v) \in E` add edges :math:`(u, v), (u, v + n)`, and
+ :math:`(u + n, v)` to M. Finally, for all vertices :math:`u \in U`, add
+ edge :math:`(u, w)` to M.
+
+ The Mycielski Operation can be done multiple times by repeating the above
+ process iteratively.
+
+ More information can be found at https://en.wikipedia.org/wiki/Mycielskian
+
+ Parameters
+ ----------
+ G : graph
+ A simple, undirected NetworkX graph
+ iterations : int
+ The number of iterations of the Mycielski operation to
+ perform on G. Defaults to 1. Must be a non-negative integer.
+
+ Returns
+ -------
+ M : graph
+ The Mycielskian of G after the specified number of iterations.
+
+ Notes
+ -----
+ Graph, node, and edge data are not necessarily propagated to the new graph.
+
+ """
+
+ M = nx.convert_node_labels_to_integers(G)
+
+ for i in range(iterations):
+ n = M.number_of_nodes()
+ M.add_nodes_from(range(n, 2 * n))
+ old_edges = list(M.edges())
+ M.add_edges_from((u, v + n) for u, v in old_edges)
+ M.add_edges_from((u + n, v) for u, v in old_edges)
+ M.add_node(2 * n)
+ M.add_edges_from((u + n, 2 * n) for u in range(n))
+
+ return M
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def mycielski_graph(n):
+ """Generator for the n_th Mycielski Graph.
+
+ The Mycielski family of graphs is an infinite set of graphs.
+ :math:`M_1` is the singleton graph, :math:`M_2` is two vertices with an
+ edge, and, for :math:`i > 2`, :math:`M_i` is the Mycielskian of
+ :math:`M_{i-1}`.
+
+ More information can be found at
+ http://mathworld.wolfram.com/MycielskiGraph.html
+
+ Parameters
+ ----------
+ n : int
+ The desired Mycielski Graph.
+
+ Returns
+ -------
+ M : graph
+ The n_th Mycielski Graph
+
+ Notes
+ -----
+ The first graph in the Mycielski sequence is the singleton graph.
+ The Mycielskian of this graph is not the :math:`P_2` graph, but rather the
+ :math:`P_2` graph with an extra, isolated vertex. The second Mycielski
+ graph is the :math:`P_2` graph, so the first two are hard coded.
+ The remaining graphs are generated using the Mycielski operation.
+
+ """
+
+ if n < 1:
+ raise nx.NetworkXError("must satisfy n >= 1")
+
+ if n == 1:
+ return nx.empty_graph(1)
+
+ else:
+ return mycielskian(nx.path_graph(2), n - 2)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/nonisomorphic_trees.py b/.venv/lib/python3.12/site-packages/networkx/generators/nonisomorphic_trees.py
new file mode 100644
index 00000000..9716cf33
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/nonisomorphic_trees.py
@@ -0,0 +1,212 @@
+"""
+Implementation of the Wright, Richmond, Odlyzko and McKay (WROM)
+algorithm for the enumeration of all non-isomorphic free trees of a
+given order. Rooted trees are represented by level sequences, i.e.,
+lists in which the i-th element specifies the distance of vertex i to
+the root.
+
+"""
+
+__all__ = ["nonisomorphic_trees", "number_of_nonisomorphic_trees"]
+
+import networkx as nx
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def nonisomorphic_trees(order, create="graph"):
+ """Generates lists of nonisomorphic trees
+
+ Parameters
+ ----------
+ order : int
+ order of the desired tree(s)
+
+ create : one of {"graph", "matrix"} (default="graph")
+ If ``"graph"`` is selected a list of ``Graph`` instances will be returned,
+ if matrix is selected a list of adjacency matrices will be returned.
+
+ .. deprecated:: 3.3
+
+ The `create` argument is deprecated and will be removed in NetworkX
+ version 3.5. In the future, `nonisomorphic_trees` will yield graph
+ instances by default. To generate adjacency matrices, call
+ ``nx.to_numpy_array`` on the output, e.g.::
+
+ [nx.to_numpy_array(G) for G in nx.nonisomorphic_trees(N)]
+
+ Yields
+ ------
+ list
+ A list of nonisomorphic trees, in one of two formats depending on the
+ value of the `create` parameter:
+ - ``create="graph"``: yields a list of `networkx.Graph` instances
+ - ``create="matrix"``: yields a list of list-of-lists representing adjacency matrices
+ """
+
+ if order < 2:
+ raise ValueError
+ # start at the path graph rooted at its center
+ layout = list(range(order // 2 + 1)) + list(range(1, (order + 1) // 2))
+
+ while layout is not None:
+ layout = _next_tree(layout)
+ if layout is not None:
+ if create == "graph":
+ yield _layout_to_graph(layout)
+ elif create == "matrix":
+ import warnings
+
+ warnings.warn(
+ (
+ "\n\nThe 'create=matrix' argument of nonisomorphic_trees\n"
+ "is deprecated and will be removed in version 3.5.\n"
+ "Use ``nx.to_numpy_array`` to convert graphs to adjacency "
+ "matrices, e.g.::\n\n"
+ " [nx.to_numpy_array(G) for G in nx.nonisomorphic_trees(N)]"
+ ),
+ category=DeprecationWarning,
+ stacklevel=2,
+ )
+
+ yield _layout_to_matrix(layout)
+ layout = _next_rooted_tree(layout)
+
+
+@nx._dispatchable(graphs=None)
+def number_of_nonisomorphic_trees(order):
+ """Returns the number of nonisomorphic trees
+
+ Parameters
+ ----------
+ order : int
+ order of the desired tree(s)
+
+ Returns
+ -------
+ length : Number of nonisomorphic graphs for the given order
+
+ References
+ ----------
+
+ """
+ return sum(1 for _ in nonisomorphic_trees(order))
+
+
+def _next_rooted_tree(predecessor, p=None):
+ """One iteration of the Beyer-Hedetniemi algorithm."""
+
+ if p is None:
+ p = len(predecessor) - 1
+ while predecessor[p] == 1:
+ p -= 1
+ if p == 0:
+ return None
+
+ q = p - 1
+ while predecessor[q] != predecessor[p] - 1:
+ q -= 1
+ result = list(predecessor)
+ for i in range(p, len(result)):
+ result[i] = result[i - p + q]
+ return result
+
+
+def _next_tree(candidate):
+ """One iteration of the Wright, Richmond, Odlyzko and McKay
+ algorithm."""
+
+ # valid representation of a free tree if:
+ # there are at least two vertices at layer 1
+ # (this is always the case because we start at the path graph)
+ left, rest = _split_tree(candidate)
+
+ # and the left subtree of the root
+ # is less high than the tree with the left subtree removed
+ left_height = max(left)
+ rest_height = max(rest)
+ valid = rest_height >= left_height
+
+ if valid and rest_height == left_height:
+ # and, if left and rest are of the same height,
+ # if left does not encompass more vertices
+ if len(left) > len(rest):
+ valid = False
+ # and, if they have the same number or vertices,
+ # if left does not come after rest lexicographically
+ elif len(left) == len(rest) and left > rest:
+ valid = False
+
+ if valid:
+ return candidate
+ else:
+ # jump to the next valid free tree
+ p = len(left)
+ new_candidate = _next_rooted_tree(candidate, p)
+ if candidate[p] > 2:
+ new_left, new_rest = _split_tree(new_candidate)
+ new_left_height = max(new_left)
+ suffix = range(1, new_left_height + 2)
+ new_candidate[-len(suffix) :] = suffix
+ return new_candidate
+
+
+def _split_tree(layout):
+ """Returns a tuple of two layouts, one containing the left
+ subtree of the root vertex, and one containing the original tree
+ with the left subtree removed."""
+
+ one_found = False
+ m = None
+ for i in range(len(layout)):
+ if layout[i] == 1:
+ if one_found:
+ m = i
+ break
+ else:
+ one_found = True
+
+ if m is None:
+ m = len(layout)
+
+ left = [layout[i] - 1 for i in range(1, m)]
+ rest = [0] + [layout[i] for i in range(m, len(layout))]
+ return (left, rest)
+
+
+def _layout_to_matrix(layout):
+ """Create the adjacency matrix for the tree specified by the
+ given layout (level sequence)."""
+
+ result = [[0] * len(layout) for i in range(len(layout))]
+ stack = []
+ for i in range(len(layout)):
+ i_level = layout[i]
+ if stack:
+ j = stack[-1]
+ j_level = layout[j]
+ while j_level >= i_level:
+ stack.pop()
+ j = stack[-1]
+ j_level = layout[j]
+ result[i][j] = result[j][i] = 1
+ stack.append(i)
+ return result
+
+
+def _layout_to_graph(layout):
+ """Create a NetworkX Graph for the tree specified by the
+ given layout(level sequence)"""
+ G = nx.Graph()
+ stack = []
+ for i in range(len(layout)):
+ i_level = layout[i]
+ if stack:
+ j = stack[-1]
+ j_level = layout[j]
+ while j_level >= i_level:
+ stack.pop()
+ j = stack[-1]
+ j_level = layout[j]
+ G.add_edge(i, j)
+ stack.append(i)
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/random_clustered.py b/.venv/lib/python3.12/site-packages/networkx/generators/random_clustered.py
new file mode 100644
index 00000000..8fbf855e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/random_clustered.py
@@ -0,0 +1,117 @@
+"""Generate graphs with given degree and triangle sequence."""
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+__all__ = ["random_clustered_graph"]
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_clustered_graph(joint_degree_sequence, create_using=None, seed=None):
+ r"""Generate a random graph with the given joint independent edge degree and
+ triangle degree sequence.
+
+ This uses a configuration model-like approach to generate a random graph
+ (with parallel edges and self-loops) by randomly assigning edges to match
+ the given joint degree sequence.
+
+ The joint degree sequence is a list of pairs of integers of the form
+ $[(d_{1,i}, d_{1,t}), \dotsc, (d_{n,i}, d_{n,t})]$. According to this list,
+ vertex $u$ is a member of $d_{u,t}$ triangles and has $d_{u, i}$ other
+ edges. The number $d_{u,t}$ is the *triangle degree* of $u$ and the number
+ $d_{u,i}$ is the *independent edge degree*.
+
+ Parameters
+ ----------
+ joint_degree_sequence : list of integer pairs
+ Each list entry corresponds to the independent edge degree and
+ triangle degree of a node.
+ create_using : NetworkX graph constructor, optional (default MultiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : MultiGraph
+ A graph with the specified degree sequence. Nodes are labeled
+ starting at 0 with an index corresponding to the position in
+ deg_sequence.
+
+ Raises
+ ------
+ NetworkXError
+ If the independent edge degree sequence sum is not even
+ or the triangle degree sequence sum is not divisible by 3.
+
+ Notes
+ -----
+ As described by Miller [1]_ (see also Newman [2]_ for an equivalent
+ description).
+
+ A non-graphical degree sequence (not realizable by some simple
+ graph) is allowed since this function returns graphs with self
+ loops and parallel edges. An exception is raised if the
+ independent degree sequence does not have an even sum or the
+ triangle degree sequence sum is not divisible by 3.
+
+ This configuration model-like construction process can lead to
+ duplicate edges and loops. You can remove the self-loops and
+ parallel edges (see below) which will likely result in a graph
+ that doesn't have the exact degree sequence specified. This
+ "finite-size effect" decreases as the size of the graph increases.
+
+ References
+ ----------
+ .. [1] Joel C. Miller. "Percolation and epidemics in random clustered
+ networks". In: Physical review. E, Statistical, nonlinear, and soft
+ matter physics 80 (2 Part 1 August 2009).
+ .. [2] M. E. J. Newman. "Random Graphs with Clustering".
+ In: Physical Review Letters 103 (5 July 2009)
+
+ Examples
+ --------
+ >>> deg = [(1, 0), (1, 0), (1, 0), (2, 0), (1, 0), (2, 1), (0, 1), (0, 1)]
+ >>> G = nx.random_clustered_graph(deg)
+
+ To remove parallel edges:
+
+ >>> G = nx.Graph(G)
+
+ To remove self loops:
+
+ >>> G.remove_edges_from(nx.selfloop_edges(G))
+
+ """
+ # In Python 3, zip() returns an iterator. Make this into a list.
+ joint_degree_sequence = list(joint_degree_sequence)
+
+ N = len(joint_degree_sequence)
+ G = nx.empty_graph(N, create_using, default=nx.MultiGraph)
+ if G.is_directed():
+ raise nx.NetworkXError("Directed Graph not supported")
+
+ ilist = []
+ tlist = []
+ for n in G:
+ degrees = joint_degree_sequence[n]
+ for icount in range(degrees[0]):
+ ilist.append(n)
+ for tcount in range(degrees[1]):
+ tlist.append(n)
+
+ if len(ilist) % 2 != 0 or len(tlist) % 3 != 0:
+ raise nx.NetworkXError("Invalid degree sequence")
+
+ seed.shuffle(ilist)
+ seed.shuffle(tlist)
+ while ilist:
+ G.add_edge(ilist.pop(), ilist.pop())
+ while tlist:
+ n1 = tlist.pop()
+ n2 = tlist.pop()
+ n3 = tlist.pop()
+ G.add_edges_from([(n1, n2), (n1, n3), (n2, n3)])
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/random_graphs.py b/.venv/lib/python3.12/site-packages/networkx/generators/random_graphs.py
new file mode 100644
index 00000000..90ae0d97
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/random_graphs.py
@@ -0,0 +1,1400 @@
+"""
+Generators for random graphs.
+
+"""
+
+import itertools
+import math
+from collections import defaultdict
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+from ..utils.misc import check_create_using
+from .classic import complete_graph, empty_graph, path_graph, star_graph
+from .degree_seq import degree_sequence_tree
+
+__all__ = [
+ "fast_gnp_random_graph",
+ "gnp_random_graph",
+ "dense_gnm_random_graph",
+ "gnm_random_graph",
+ "erdos_renyi_graph",
+ "binomial_graph",
+ "newman_watts_strogatz_graph",
+ "watts_strogatz_graph",
+ "connected_watts_strogatz_graph",
+ "random_regular_graph",
+ "barabasi_albert_graph",
+ "dual_barabasi_albert_graph",
+ "extended_barabasi_albert_graph",
+ "powerlaw_cluster_graph",
+ "random_lobster",
+ "random_shell_graph",
+ "random_powerlaw_tree",
+ "random_powerlaw_tree_sequence",
+ "random_kernel_graph",
+]
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def fast_gnp_random_graph(n, p, seed=None, directed=False, *, create_using=None):
+ """Returns a $G_{n,p}$ random graph, also known as an Erdős-Rényi graph or
+ a binomial graph.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ 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, this function returns a directed graph.
+ create_using : Graph constructor, optional (default=nx.Graph or nx.DiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph types are not supported and raise a ``NetworkXError``.
+ By default NetworkX Graph or DiGraph are used depending on `directed`.
+
+ Notes
+ -----
+ The $G_{n,p}$ graph algorithm chooses each of the $[n (n - 1)] / 2$
+ (undirected) or $n (n - 1)$ (directed) possible edges with probability $p$.
+
+ This algorithm [1]_ runs in $O(n + m)$ time, where `m` is the expected number of
+ edges, which equals $p n (n - 1) / 2$. This should be faster than
+ :func:`gnp_random_graph` when $p$ is small and the expected number of edges
+ is small (that is, the graph is sparse).
+
+ See Also
+ --------
+ gnp_random_graph
+
+ References
+ ----------
+ .. [1] Vladimir Batagelj and Ulrik Brandes,
+ "Efficient generation of large random networks",
+ Phys. Rev. E, 71, 036113, 2005.
+ """
+ default = nx.DiGraph if directed else nx.Graph
+ create_using = check_create_using(
+ create_using, directed=directed, multigraph=False, default=default
+ )
+ if p <= 0 or p >= 1:
+ return nx.gnp_random_graph(
+ n, p, seed=seed, directed=directed, create_using=create_using
+ )
+
+ G = empty_graph(n, create_using=create_using)
+
+ lp = math.log(1.0 - p)
+
+ if directed:
+ v = 1
+ w = -1
+ while v < n:
+ lr = math.log(1.0 - seed.random())
+ w = w + 1 + int(lr / lp)
+ while w >= v and v < n:
+ w = w - v
+ v = v + 1
+ if v < n:
+ G.add_edge(w, v)
+
+ # Nodes in graph are from 0,n-1 (start with v as the second node index).
+ v = 1
+ w = -1
+ while v < n:
+ lr = math.log(1.0 - seed.random())
+ w = w + 1 + int(lr / lp)
+ while w >= v and v < n:
+ w = w - v
+ v = v + 1
+ if v < n:
+ G.add_edge(v, w)
+ return G
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def gnp_random_graph(n, p, seed=None, directed=False, *, create_using=None):
+ """Returns a $G_{n,p}$ random graph, also known as an Erdős-Rényi graph
+ or a binomial graph.
+
+ The $G_{n,p}$ model chooses each of the possible edges with probability $p$.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ 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, this function returns a directed graph.
+ create_using : Graph constructor, optional (default=nx.Graph or nx.DiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph types are not supported and raise a ``NetworkXError``.
+ By default NetworkX Graph or DiGraph are used depending on `directed`.
+
+ See Also
+ --------
+ fast_gnp_random_graph
+
+ Notes
+ -----
+ This algorithm [2]_ runs in $O(n^2)$ time. For sparse graphs (that is, for
+ small values of $p$), :func:`fast_gnp_random_graph` is a faster algorithm.
+
+ :func:`binomial_graph` and :func:`erdos_renyi_graph` are
+ aliases for :func:`gnp_random_graph`.
+
+ >>> nx.binomial_graph is nx.gnp_random_graph
+ True
+ >>> nx.erdos_renyi_graph is nx.gnp_random_graph
+ True
+
+ References
+ ----------
+ .. [1] P. Erdős and A. Rényi, On Random Graphs, Publ. Math. 6, 290 (1959).
+ .. [2] E. N. Gilbert, Random Graphs, Ann. Math. Stat., 30, 1141 (1959).
+ """
+ default = nx.DiGraph if directed else nx.Graph
+ create_using = check_create_using(
+ create_using, directed=directed, multigraph=False, default=default
+ )
+ if p >= 1:
+ return complete_graph(n, create_using=create_using)
+
+ G = nx.empty_graph(n, create_using=create_using)
+ if p <= 0:
+ return G
+
+ edgetool = itertools.permutations if directed else itertools.combinations
+ for e in edgetool(range(n), 2):
+ if seed.random() < p:
+ G.add_edge(*e)
+ return G
+
+
+# add some aliases to common names
+binomial_graph = gnp_random_graph
+erdos_renyi_graph = gnp_random_graph
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def dense_gnm_random_graph(n, m, seed=None, *, create_using=None):
+ """Returns a $G_{n,m}$ random graph.
+
+ In the $G_{n,m}$ model, a graph is chosen uniformly at random from the set
+ of all graphs with $n$ nodes and $m$ edges.
+
+ This algorithm should be faster than :func:`gnm_random_graph` for dense
+ graphs.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ m : int
+ The number of edges.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ See Also
+ --------
+ gnm_random_graph
+
+ Notes
+ -----
+ Algorithm by Keith M. Briggs Mar 31, 2006.
+ Inspired by Knuth's Algorithm S (Selection sampling technique),
+ in section 3.4.2 of [1]_.
+
+ References
+ ----------
+ .. [1] Donald E. Knuth, The Art of Computer Programming,
+ Volume 2/Seminumerical algorithms, Third Edition, Addison-Wesley, 1997.
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ mmax = n * (n - 1) // 2
+ if m >= mmax:
+ return complete_graph(n, create_using)
+ G = empty_graph(n, create_using)
+
+ if n == 1:
+ return G
+
+ u = 0
+ v = 1
+ t = 0
+ k = 0
+ while True:
+ if seed.randrange(mmax - t) < m - k:
+ G.add_edge(u, v)
+ k += 1
+ if k == m:
+ return G
+ t += 1
+ v += 1
+ if v == n: # go to next row of adjacency matrix
+ u += 1
+ v = u + 1
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def gnm_random_graph(n, m, seed=None, directed=False, *, create_using=None):
+ """Returns a $G_{n,m}$ random graph.
+
+ In the $G_{n,m}$ model, a graph is chosen uniformly at random from the set
+ of all graphs with $n$ nodes and $m$ edges.
+
+ This algorithm should be faster than :func:`dense_gnm_random_graph` for
+ sparse graphs.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ m : 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
+ create_using : Graph constructor, optional (default=nx.Graph or nx.DiGraph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph types are not supported and raise a ``NetworkXError``.
+ By default NetworkX Graph or DiGraph are used depending on `directed`.
+
+ See also
+ --------
+ dense_gnm_random_graph
+
+ """
+ default = nx.DiGraph if directed else nx.Graph
+ create_using = check_create_using(
+ create_using, directed=directed, multigraph=False, default=default
+ )
+ if n == 1:
+ return nx.empty_graph(n, create_using=create_using)
+ max_edges = n * (n - 1) if directed else n * (n - 1) / 2.0
+ if m >= max_edges:
+ return complete_graph(n, create_using=create_using)
+
+ G = nx.empty_graph(n, create_using=create_using)
+ nlist = list(G)
+ edge_count = 0
+ while edge_count < m:
+ # generate random edge,u,v
+ u = seed.choice(nlist)
+ v = seed.choice(nlist)
+ if u == v or G.has_edge(u, v):
+ continue
+ else:
+ G.add_edge(u, v)
+ edge_count = edge_count + 1
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def newman_watts_strogatz_graph(n, k, p, seed=None, *, create_using=None):
+ """Returns a Newman–Watts–Strogatz small-world graph.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ k : int
+ Each node is joined with its `k` nearest neighbors in a ring
+ topology.
+ p : float
+ The probability of adding a new edge for each edge.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Notes
+ -----
+ First create a ring over $n$ nodes [1]_. Then each node in the ring is
+ connected with its $k$ nearest neighbors (or $k - 1$ neighbors if $k$
+ is odd). Then shortcuts are created by adding new edges as follows: for
+ each edge $(u, v)$ in the underlying "$n$-ring with $k$ nearest
+ neighbors" with probability $p$ add a new edge $(u, w)$ with
+ randomly-chosen existing node $w$. In contrast with
+ :func:`watts_strogatz_graph`, no edges are removed.
+
+ See Also
+ --------
+ watts_strogatz_graph
+
+ References
+ ----------
+ .. [1] M. E. J. Newman and D. J. Watts,
+ Renormalization group analysis of the small-world network model,
+ Physics Letters A, 263, 341, 1999.
+ https://doi.org/10.1016/S0375-9601(99)00757-4
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ if k > n:
+ raise nx.NetworkXError("k>=n, choose smaller k or larger n")
+
+ # If k == n the graph return is a complete graph
+ if k == n:
+ return nx.complete_graph(n, create_using)
+
+ G = empty_graph(n, create_using)
+ nlist = list(G.nodes())
+ fromv = nlist
+ # connect the k/2 neighbors
+ for j in range(1, k // 2 + 1):
+ tov = fromv[j:] + fromv[0:j] # the first j are now last
+ for i in range(len(fromv)):
+ G.add_edge(fromv[i], tov[i])
+ # for each edge u-v, with probability p, randomly select existing
+ # node w and add new edge u-w
+ e = list(G.edges())
+ for u, v in e:
+ if seed.random() < p:
+ w = seed.choice(nlist)
+ # no self-loops and reject if edge u-w exists
+ # is that the correct NWS model?
+ while w == u or G.has_edge(u, w):
+ w = seed.choice(nlist)
+ if G.degree(u) >= n - 1:
+ break # skip this rewiring
+ else:
+ G.add_edge(u, w)
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def watts_strogatz_graph(n, k, p, seed=None, *, create_using=None):
+ """Returns a Watts–Strogatz small-world graph.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes
+ k : int
+ Each node is joined with its `k` nearest neighbors in a ring
+ topology.
+ p : float
+ The probability of rewiring each edge
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ See Also
+ --------
+ newman_watts_strogatz_graph
+ connected_watts_strogatz_graph
+
+ Notes
+ -----
+ First create a ring over $n$ nodes [1]_. Then each node in the ring is joined
+ to its $k$ nearest neighbors (or $k - 1$ neighbors if $k$ is odd).
+ Then shortcuts are created by replacing some edges as follows: for each
+ edge $(u, v)$ in the underlying "$n$-ring with $k$ nearest neighbors"
+ with probability $p$ replace it with a new edge $(u, w)$ with uniformly
+ random choice of existing node $w$.
+
+ In contrast with :func:`newman_watts_strogatz_graph`, the random rewiring
+ does not increase the number of edges. The rewired graph is not guaranteed
+ to be connected as in :func:`connected_watts_strogatz_graph`.
+
+ References
+ ----------
+ .. [1] Duncan J. Watts and Steven H. Strogatz,
+ Collective dynamics of small-world networks,
+ Nature, 393, pp. 440--442, 1998.
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ if k > n:
+ raise nx.NetworkXError("k>n, choose smaller k or larger n")
+
+ # If k == n, the graph is complete not Watts-Strogatz
+ if k == n:
+ G = nx.complete_graph(n, create_using)
+ return G
+
+ G = nx.empty_graph(n, create_using=create_using)
+ nodes = list(range(n)) # nodes are labeled 0 to n-1
+ # connect each node to k/2 neighbors
+ for j in range(1, k // 2 + 1):
+ targets = nodes[j:] + nodes[0:j] # first j nodes are now last in list
+ G.add_edges_from(zip(nodes, targets))
+ # rewire edges from each node
+ # loop over all nodes in order (label) and neighbors in order (distance)
+ # no self loops or multiple edges allowed
+ for j in range(1, k // 2 + 1): # outer loop is neighbors
+ targets = nodes[j:] + nodes[0:j] # first j nodes are now last in list
+ # inner loop in node order
+ for u, v in zip(nodes, targets):
+ if seed.random() < p:
+ w = seed.choice(nodes)
+ # Enforce no self-loops or multiple edges
+ while w == u or G.has_edge(u, w):
+ w = seed.choice(nodes)
+ if G.degree(u) >= n - 1:
+ break # skip this rewiring
+ else:
+ G.remove_edge(u, v)
+ G.add_edge(u, w)
+ return G
+
+
+@py_random_state(4)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def connected_watts_strogatz_graph(n, k, p, tries=100, seed=None, *, create_using=None):
+ """Returns a connected Watts–Strogatz small-world graph.
+
+ Attempts to generate a connected graph by repeated generation of
+ Watts–Strogatz small-world graphs. An exception is raised if the maximum
+ number of tries is exceeded.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes
+ k : int
+ Each node is joined with its `k` nearest neighbors in a ring
+ topology.
+ p : float
+ The probability of rewiring each edge
+ tries : int
+ Number of attempts to generate a connected graph.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Notes
+ -----
+ First create a ring over $n$ nodes [1]_. Then each node in the ring is joined
+ to its $k$ nearest neighbors (or $k - 1$ neighbors if $k$ is odd).
+ Then shortcuts are created by replacing some edges as follows: for each
+ edge $(u, v)$ in the underlying "$n$-ring with $k$ nearest neighbors"
+ with probability $p$ replace it with a new edge $(u, w)$ with uniformly
+ random choice of existing node $w$.
+ The entire process is repeated until a connected graph results.
+
+ See Also
+ --------
+ newman_watts_strogatz_graph
+ watts_strogatz_graph
+
+ References
+ ----------
+ .. [1] Duncan J. Watts and Steven H. Strogatz,
+ Collective dynamics of small-world networks,
+ Nature, 393, pp. 440--442, 1998.
+ """
+ for i in range(tries):
+ # seed is an RNG so should change sequence each call
+ G = watts_strogatz_graph(n, k, p, seed, create_using=create_using)
+ if nx.is_connected(G):
+ return G
+ raise nx.NetworkXError("Maximum number of tries exceeded")
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_regular_graph(d, n, seed=None, *, create_using=None):
+ r"""Returns a random $d$-regular graph on $n$ nodes.
+
+ A regular graph is a graph where each node has the same number of neighbors.
+
+ The resulting graph has no self-loops or parallel edges.
+
+ Parameters
+ ----------
+ d : int
+ The degree of each node.
+ n : integer
+ The number of nodes. The value of $n \times d$ must be even.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Notes
+ -----
+ The nodes are numbered from $0$ to $n - 1$.
+
+ Kim and Vu's paper [2]_ shows that this algorithm samples in an
+ asymptotically uniform way from the space of random graphs when
+ $d = O(n^{1 / 3 - \epsilon})$.
+
+ Raises
+ ------
+
+ NetworkXError
+ If $n \times d$ is odd or $d$ is greater than or equal to $n$.
+
+ References
+ ----------
+ .. [1] A. Steger and N. Wormald,
+ Generating random regular graphs quickly,
+ Probability and Computing 8 (1999), 377-396, 1999.
+ https://doi.org/10.1017/S0963548399003867
+
+ .. [2] Jeong Han Kim and Van H. Vu,
+ Generating random regular graphs,
+ Proceedings of the thirty-fifth ACM symposium on Theory of computing,
+ San Diego, CA, USA, pp 213--222, 2003.
+ http://portal.acm.org/citation.cfm?id=780542.780576
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ if (n * d) % 2 != 0:
+ raise nx.NetworkXError("n * d must be even")
+
+ if not 0 <= d < n:
+ raise nx.NetworkXError("the 0 <= d < n inequality must be satisfied")
+
+ G = nx.empty_graph(n, create_using=create_using)
+
+ if d == 0:
+ return G
+
+ def _suitable(edges, potential_edges):
+ # Helper subroutine to check if there are suitable edges remaining
+ # If False, the generation of the graph has failed
+ if not potential_edges:
+ return True
+ for s1 in potential_edges:
+ for s2 in potential_edges:
+ # Two iterators on the same dictionary are guaranteed
+ # to visit it in the same order if there are no
+ # intervening modifications.
+ if s1 == s2:
+ # Only need to consider s1-s2 pair one time
+ break
+ if s1 > s2:
+ s1, s2 = s2, s1
+ if (s1, s2) not in edges:
+ return True
+ return False
+
+ def _try_creation():
+ # Attempt to create an edge set
+
+ edges = set()
+ stubs = list(range(n)) * d
+
+ while stubs:
+ potential_edges = defaultdict(lambda: 0)
+ seed.shuffle(stubs)
+ stubiter = iter(stubs)
+ for s1, s2 in zip(stubiter, stubiter):
+ if s1 > s2:
+ s1, s2 = s2, s1
+ if s1 != s2 and ((s1, s2) not in edges):
+ edges.add((s1, s2))
+ else:
+ potential_edges[s1] += 1
+ potential_edges[s2] += 1
+
+ if not _suitable(edges, potential_edges):
+ return None # failed to find suitable edge set
+
+ stubs = [
+ node
+ for node, potential in potential_edges.items()
+ for _ in range(potential)
+ ]
+ return edges
+
+ # Even though a suitable edge set exists,
+ # the generation of such a set is not guaranteed.
+ # Try repeatedly to find one.
+ edges = _try_creation()
+ while edges is None:
+ edges = _try_creation()
+ G.add_edges_from(edges)
+
+ return G
+
+
+def _random_subset(seq, m, rng):
+ """Return m unique elements from seq.
+
+ This differs from random.sample which can return repeated
+ elements if seq holds repeated elements.
+
+ Note: rng is a random.Random or numpy.random.RandomState instance.
+ """
+ targets = set()
+ while len(targets) < m:
+ x = rng.choice(seq)
+ targets.add(x)
+ return targets
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def barabasi_albert_graph(n, m, seed=None, initial_graph=None, *, create_using=None):
+ """Returns a random graph using Barabási–Albert preferential attachment
+
+ A graph of $n$ nodes is grown by attaching new nodes each with $m$
+ edges that are preferentially attached to existing nodes with high degree.
+
+ Parameters
+ ----------
+ n : int
+ Number of nodes
+ m : int
+ Number of edges to attach from a new node to existing nodes
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ initial_graph : Graph or None (default)
+ Initial network for Barabási–Albert algorithm.
+ It should be a connected graph for most use cases.
+ A copy of `initial_graph` is used.
+ If None, starts from a star graph on (m+1) nodes.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Returns
+ -------
+ G : Graph
+
+ Raises
+ ------
+ NetworkXError
+ If `m` does not satisfy ``1 <= m < n``, or
+ the initial graph number of nodes m0 does not satisfy ``m <= m0 <= n``.
+
+ References
+ ----------
+ .. [1] A. L. Barabási and R. Albert "Emergence of scaling in
+ random networks", Science 286, pp 509-512, 1999.
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ if m < 1 or m >= n:
+ raise nx.NetworkXError(
+ f"Barabási–Albert network must have m >= 1 and m < n, m = {m}, n = {n}"
+ )
+
+ if initial_graph is None:
+ # Default initial graph : star graph on (m + 1) nodes
+ G = star_graph(m, create_using)
+ else:
+ if len(initial_graph) < m or len(initial_graph) > n:
+ raise nx.NetworkXError(
+ f"Barabási–Albert initial graph needs between m={m} and n={n} nodes"
+ )
+ G = initial_graph.copy()
+
+ # List of existing nodes, with nodes repeated once for each adjacent edge
+ repeated_nodes = [n for n, d in G.degree() for _ in range(d)]
+ # Start adding the other n - m0 nodes.
+ source = len(G)
+ while source < n:
+ # Now choose m unique nodes from the existing nodes
+ # Pick uniformly from repeated_nodes (preferential attachment)
+ targets = _random_subset(repeated_nodes, m, seed)
+ # Add edges to m nodes from the source.
+ G.add_edges_from(zip([source] * m, targets))
+ # Add one node to the list for each new edge just created.
+ repeated_nodes.extend(targets)
+ # And the new node "source" has m edges to add to the list.
+ repeated_nodes.extend([source] * m)
+
+ source += 1
+ return G
+
+
+@py_random_state(4)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def dual_barabasi_albert_graph(
+ n, m1, m2, p, seed=None, initial_graph=None, *, create_using=None
+):
+ """Returns a random graph using dual Barabási–Albert preferential attachment
+
+ A graph of $n$ nodes is grown by attaching new nodes each with either $m_1$
+ edges (with probability $p$) or $m_2$ edges (with probability $1-p$) that
+ are preferentially attached to existing nodes with high degree.
+
+ Parameters
+ ----------
+ n : int
+ Number of nodes
+ m1 : int
+ Number of edges to link each new node to existing nodes with probability $p$
+ m2 : int
+ Number of edges to link each new node to existing nodes with probability $1-p$
+ p : float
+ The probability of attaching $m_1$ edges (as opposed to $m_2$ edges)
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ initial_graph : Graph or None (default)
+ Initial network for Barabási–Albert algorithm.
+ A copy of `initial_graph` is used.
+ It should be connected for most use cases.
+ If None, starts from an star graph on max(m1, m2) + 1 nodes.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Returns
+ -------
+ G : Graph
+
+ Raises
+ ------
+ NetworkXError
+ If `m1` and `m2` do not satisfy ``1 <= m1,m2 < n``, or
+ `p` does not satisfy ``0 <= p <= 1``, or
+ the initial graph number of nodes m0 does not satisfy m1, m2 <= m0 <= n.
+
+ References
+ ----------
+ .. [1] N. Moshiri "The dual-Barabasi-Albert model", arXiv:1810.10538.
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ if m1 < 1 or m1 >= n:
+ raise nx.NetworkXError(
+ f"Dual Barabási–Albert must have m1 >= 1 and m1 < n, m1 = {m1}, n = {n}"
+ )
+ if m2 < 1 or m2 >= n:
+ raise nx.NetworkXError(
+ f"Dual Barabási–Albert must have m2 >= 1 and m2 < n, m2 = {m2}, n = {n}"
+ )
+ if p < 0 or p > 1:
+ raise nx.NetworkXError(
+ f"Dual Barabási–Albert network must have 0 <= p <= 1, p = {p}"
+ )
+
+ # For simplicity, if p == 0 or 1, just return BA
+ if p == 1:
+ return barabasi_albert_graph(n, m1, seed, create_using=create_using)
+ elif p == 0:
+ return barabasi_albert_graph(n, m2, seed, create_using=create_using)
+
+ if initial_graph is None:
+ # Default initial graph : star graph on max(m1, m2) nodes
+ G = star_graph(max(m1, m2), create_using)
+ else:
+ if len(initial_graph) < max(m1, m2) or len(initial_graph) > n:
+ raise nx.NetworkXError(
+ f"Barabási–Albert initial graph must have between "
+ f"max(m1, m2) = {max(m1, m2)} and n = {n} nodes"
+ )
+ G = initial_graph.copy()
+
+ # Target nodes for new edges
+ targets = list(G)
+ # List of existing nodes, with nodes repeated once for each adjacent edge
+ repeated_nodes = [n for n, d in G.degree() for _ in range(d)]
+ # Start adding the remaining nodes.
+ source = len(G)
+ while source < n:
+ # Pick which m to use (m1 or m2)
+ if seed.random() < p:
+ m = m1
+ else:
+ m = m2
+ # Now choose m unique nodes from the existing nodes
+ # Pick uniformly from repeated_nodes (preferential attachment)
+ targets = _random_subset(repeated_nodes, m, seed)
+ # Add edges to m nodes from the source.
+ G.add_edges_from(zip([source] * m, targets))
+ # Add one node to the list for each new edge just created.
+ repeated_nodes.extend(targets)
+ # And the new node "source" has m edges to add to the list.
+ repeated_nodes.extend([source] * m)
+
+ source += 1
+ return G
+
+
+@py_random_state(4)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def extended_barabasi_albert_graph(n, m, p, q, seed=None, *, create_using=None):
+ """Returns an extended Barabási–Albert model graph.
+
+ An extended Barabási–Albert model graph is a random graph constructed
+ using preferential attachment. The extended model allows new edges,
+ rewired edges or new nodes. Based on the probabilities $p$ and $q$
+ with $p + q < 1$, the growing behavior of the graph is determined as:
+
+ 1) With $p$ probability, $m$ new edges are added to the graph,
+ starting from randomly chosen existing nodes and attached preferentially at the
+ other end.
+
+ 2) With $q$ probability, $m$ existing edges are rewired
+ by randomly choosing an edge and rewiring one end to a preferentially chosen node.
+
+ 3) With $(1 - p - q)$ probability, $m$ new nodes are added to the graph
+ with edges attached preferentially.
+
+ When $p = q = 0$, the model behaves just like the Barabási–Alber model.
+
+ Parameters
+ ----------
+ n : int
+ Number of nodes
+ m : int
+ Number of edges with which a new node attaches to existing nodes
+ p : float
+ Probability value for adding an edge between existing nodes. p + q < 1
+ q : float
+ Probability value of rewiring of existing edges. p + q < 1
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Returns
+ -------
+ G : Graph
+
+ Raises
+ ------
+ NetworkXError
+ If `m` does not satisfy ``1 <= m < n`` or ``1 >= p + q``
+
+ References
+ ----------
+ .. [1] Albert, R., & Barabási, A. L. (2000)
+ Topology of evolving networks: local events and universality
+ Physical review letters, 85(24), 5234.
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ if m < 1 or m >= n:
+ msg = f"Extended Barabasi-Albert network needs m>=1 and m<n, m={m}, n={n}"
+ raise nx.NetworkXError(msg)
+ if p + q >= 1:
+ msg = f"Extended Barabasi-Albert network needs p + q <= 1, p={p}, q={q}"
+ raise nx.NetworkXError(msg)
+
+ # Add m initial nodes (m0 in barabasi-speak)
+ G = empty_graph(m, create_using)
+
+ # List of nodes to represent the preferential attachment random selection.
+ # At the creation of the graph, all nodes are added to the list
+ # so that even nodes that are not connected have a chance to get selected,
+ # for rewiring and adding of edges.
+ # With each new edge, nodes at the ends of the edge are added to the list.
+ attachment_preference = []
+ attachment_preference.extend(range(m))
+
+ # Start adding the other n-m nodes. The first node is m.
+ new_node = m
+ while new_node < n:
+ a_probability = seed.random()
+
+ # Total number of edges of a Clique of all the nodes
+ clique_degree = len(G) - 1
+ clique_size = (len(G) * clique_degree) / 2
+
+ # Adding m new edges, if there is room to add them
+ if a_probability < p and G.size() <= clique_size - m:
+ # Select the nodes where an edge can be added
+ eligible_nodes = [nd for nd, deg in G.degree() if deg < clique_degree]
+ for i in range(m):
+ # Choosing a random source node from eligible_nodes
+ src_node = seed.choice(eligible_nodes)
+
+ # Picking a possible node that is not 'src_node' or
+ # neighbor with 'src_node', with preferential attachment
+ prohibited_nodes = list(G[src_node])
+ prohibited_nodes.append(src_node)
+ # This will raise an exception if the sequence is empty
+ dest_node = seed.choice(
+ [nd for nd in attachment_preference if nd not in prohibited_nodes]
+ )
+ # Adding the new edge
+ G.add_edge(src_node, dest_node)
+
+ # Appending both nodes to add to their preferential attachment
+ attachment_preference.append(src_node)
+ attachment_preference.append(dest_node)
+
+ # Adjusting the eligible nodes. Degree may be saturated.
+ if G.degree(src_node) == clique_degree:
+ eligible_nodes.remove(src_node)
+ if G.degree(dest_node) == clique_degree and dest_node in eligible_nodes:
+ eligible_nodes.remove(dest_node)
+
+ # Rewiring m edges, if there are enough edges
+ elif p <= a_probability < (p + q) and m <= G.size() < clique_size:
+ # Selecting nodes that have at least 1 edge but that are not
+ # fully connected to ALL other nodes (center of star).
+ # These nodes are the pivot nodes of the edges to rewire
+ eligible_nodes = [nd for nd, deg in G.degree() if 0 < deg < clique_degree]
+ for i in range(m):
+ # Choosing a random source node
+ node = seed.choice(eligible_nodes)
+
+ # The available nodes do have a neighbor at least.
+ nbr_nodes = list(G[node])
+
+ # Choosing the other end that will get detached
+ src_node = seed.choice(nbr_nodes)
+
+ # Picking a target node that is not 'node' or
+ # neighbor with 'node', with preferential attachment
+ nbr_nodes.append(node)
+ dest_node = seed.choice(
+ [nd for nd in attachment_preference if nd not in nbr_nodes]
+ )
+ # Rewire
+ G.remove_edge(node, src_node)
+ G.add_edge(node, dest_node)
+
+ # Adjusting the preferential attachment list
+ attachment_preference.remove(src_node)
+ attachment_preference.append(dest_node)
+
+ # Adjusting the eligible nodes.
+ # nodes may be saturated or isolated.
+ if G.degree(src_node) == 0 and src_node in eligible_nodes:
+ eligible_nodes.remove(src_node)
+ if dest_node in eligible_nodes:
+ if G.degree(dest_node) == clique_degree:
+ eligible_nodes.remove(dest_node)
+ else:
+ if G.degree(dest_node) == 1:
+ eligible_nodes.append(dest_node)
+
+ # Adding new node with m edges
+ else:
+ # Select the edges' nodes by preferential attachment
+ targets = _random_subset(attachment_preference, m, seed)
+ G.add_edges_from(zip([new_node] * m, targets))
+
+ # Add one node to the list for each new edge just created.
+ attachment_preference.extend(targets)
+ # The new node has m edges to it, plus itself: m + 1
+ attachment_preference.extend([new_node] * (m + 1))
+ new_node += 1
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def powerlaw_cluster_graph(n, m, p, seed=None, *, create_using=None):
+ """Holme and Kim algorithm for growing graphs with powerlaw
+ degree distribution and approximate average clustering.
+
+ Parameters
+ ----------
+ n : int
+ the number of nodes
+ m : int
+ the number of random edges to add for each new node
+ p : float,
+ Probability of adding a triangle after adding a random edge
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Notes
+ -----
+ The average clustering has a hard time getting above a certain
+ cutoff that depends on `m`. This cutoff is often quite low. The
+ transitivity (fraction of triangles to possible triangles) seems to
+ decrease with network size.
+
+ It is essentially the Barabási–Albert (BA) growth model with an
+ extra step that each random edge is followed by a chance of
+ making an edge to one of its neighbors too (and thus a triangle).
+
+ This algorithm improves on BA in the sense that it enables a
+ higher average clustering to be attained if desired.
+
+ It seems possible to have a disconnected graph with this algorithm
+ since the initial `m` nodes may not be all linked to a new node
+ on the first iteration like the BA model.
+
+ Raises
+ ------
+ NetworkXError
+ If `m` does not satisfy ``1 <= m <= n`` or `p` does not
+ satisfy ``0 <= p <= 1``.
+
+ References
+ ----------
+ .. [1] P. Holme and B. J. Kim,
+ "Growing scale-free networks with tunable clustering",
+ Phys. Rev. E, 65, 026107, 2002.
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ if m < 1 or n < m:
+ raise nx.NetworkXError(f"NetworkXError must have m>1 and m<n, m={m},n={n}")
+
+ if p > 1 or p < 0:
+ raise nx.NetworkXError(f"NetworkXError p must be in [0,1], p={p}")
+
+ G = empty_graph(m, create_using) # add m initial nodes (m0 in barabasi-speak)
+ repeated_nodes = list(G) # list of existing nodes to sample from
+ # with nodes repeated once for each adjacent edge
+ source = m # next node is m
+ while source < n: # Now add the other n-1 nodes
+ possible_targets = _random_subset(repeated_nodes, m, seed)
+ # do one preferential attachment for new node
+ target = possible_targets.pop()
+ G.add_edge(source, target)
+ repeated_nodes.append(target) # add one node to list for each new link
+ count = 1
+ while count < m: # add m-1 more new links
+ if seed.random() < p: # clustering step: add triangle
+ neighborhood = [
+ nbr
+ for nbr in G.neighbors(target)
+ if not G.has_edge(source, nbr) and nbr != source
+ ]
+ if neighborhood: # if there is a neighbor without a link
+ nbr = seed.choice(neighborhood)
+ G.add_edge(source, nbr) # add triangle
+ repeated_nodes.append(nbr)
+ count = count + 1
+ continue # go to top of while loop
+ # else do preferential attachment step if above fails
+ target = possible_targets.pop()
+ G.add_edge(source, target)
+ repeated_nodes.append(target)
+ count = count + 1
+
+ repeated_nodes.extend([source] * m) # add source node to list m times
+ source += 1
+ return G
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_lobster(n, p1, p2, seed=None, *, create_using=None):
+ """Returns a random lobster graph.
+
+ A lobster is a tree that reduces to a caterpillar when pruning all
+ leaf nodes. A caterpillar is a tree that reduces to a path graph
+ when pruning all leaf nodes; setting `p2` to zero produces a caterpillar.
+
+ This implementation iterates on the probabilities `p1` and `p2` to add
+ edges at levels 1 and 2, respectively. Graphs are therefore constructed
+ iteratively with uniform randomness at each level rather than being selected
+ uniformly at random from the set of all possible lobsters.
+
+ Parameters
+ ----------
+ n : int
+ The expected number of nodes in the backbone
+ p1 : float
+ Probability of adding an edge to the backbone
+ p2 : float
+ Probability of adding an edge one level beyond backbone
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Grap)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Raises
+ ------
+ NetworkXError
+ If `p1` or `p2` parameters are >= 1 because the while loops would never finish.
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ p1, p2 = abs(p1), abs(p2)
+ if any(p >= 1 for p in [p1, p2]):
+ raise nx.NetworkXError("Probability values for `p1` and `p2` must both be < 1.")
+
+ # a necessary ingredient in any self-respecting graph library
+ llen = int(2 * seed.random() * n + 0.5)
+ L = path_graph(llen, create_using)
+ # build caterpillar: add edges to path graph with probability p1
+ current_node = llen - 1
+ for n in range(llen):
+ while seed.random() < p1: # add fuzzy caterpillar parts
+ current_node += 1
+ L.add_edge(n, current_node)
+ cat_node = current_node
+ while seed.random() < p2: # add crunchy lobster bits
+ current_node += 1
+ L.add_edge(cat_node, current_node)
+ return L # voila, un lobster!
+
+
+@py_random_state(1)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_shell_graph(constructor, seed=None, *, create_using=None):
+ """Returns a random shell graph for the constructor given.
+
+ Parameters
+ ----------
+ constructor : list of three-tuples
+ Represents the parameters for a shell, starting at the center
+ shell. Each element of the list must be of the form `(n, m,
+ d)`, where `n` is the number of nodes in the shell, `m` is
+ the number of edges in the shell, and `d` is the ratio of
+ inter-shell (next) edges to intra-shell edges. If `d` is zero,
+ there will be no intra-shell edges, and if `d` is one there
+ will be all possible intra-shell edges.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. Graph instances are not supported.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Examples
+ --------
+ >>> constructor = [(10, 20, 0.8), (20, 40, 0.8)]
+ >>> G = nx.random_shell_graph(constructor)
+
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ G = empty_graph(0, create_using)
+
+ glist = []
+ intra_edges = []
+ nnodes = 0
+ # create gnm graphs for each shell
+ for n, m, d in constructor:
+ inter_edges = int(m * d)
+ intra_edges.append(m - inter_edges)
+ g = nx.convert_node_labels_to_integers(
+ gnm_random_graph(n, inter_edges, seed=seed, create_using=G.__class__),
+ first_label=nnodes,
+ )
+ glist.append(g)
+ nnodes += n
+ G = nx.operators.union(G, g)
+
+ # connect the shells randomly
+ for gi in range(len(glist) - 1):
+ nlist1 = list(glist[gi])
+ nlist2 = list(glist[gi + 1])
+ total_edges = intra_edges[gi]
+ edge_count = 0
+ while edge_count < total_edges:
+ u = seed.choice(nlist1)
+ v = seed.choice(nlist2)
+ if u == v or G.has_edge(u, v):
+ continue
+ else:
+ G.add_edge(u, v)
+ edge_count = edge_count + 1
+ return G
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_powerlaw_tree(n, gamma=3, seed=None, tries=100, *, create_using=None):
+ """Returns a tree with a power law degree distribution.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ gamma : float
+ Exponent of the power law.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ tries : int
+ Number of attempts to adjust the sequence to make it a tree.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Raises
+ ------
+ NetworkXError
+ If no valid sequence is found within the maximum number of
+ attempts.
+
+ Notes
+ -----
+ A trial power law degree sequence is chosen and then elements are
+ swapped with new elements from a powerlaw distribution until the
+ sequence makes a tree (by checking, for example, that the number of
+ edges is one smaller than the number of nodes).
+
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ # This call may raise a NetworkXError if the number of tries is succeeded.
+ seq = random_powerlaw_tree_sequence(n, gamma=gamma, seed=seed, tries=tries)
+ G = degree_sequence_tree(seq, create_using)
+ return G
+
+
+@py_random_state(2)
+@nx._dispatchable(graphs=None)
+def random_powerlaw_tree_sequence(n, gamma=3, seed=None, tries=100):
+ """Returns a degree sequence for a tree with a power law distribution.
+
+ Parameters
+ ----------
+ n : int,
+ The number of nodes.
+ gamma : float
+ Exponent of the power law.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ tries : int
+ Number of attempts to adjust the sequence to make it a tree.
+
+ Raises
+ ------
+ NetworkXError
+ If no valid sequence is found within the maximum number of
+ attempts.
+
+ Notes
+ -----
+ A trial power law degree sequence is chosen and then elements are
+ swapped with new elements from a power law distribution until
+ the sequence makes a tree (by checking, for example, that the number of
+ edges is one smaller than the number of nodes).
+
+ """
+ # get trial sequence
+ z = nx.utils.powerlaw_sequence(n, exponent=gamma, seed=seed)
+ # round to integer values in the range [0,n]
+ zseq = [min(n, max(round(s), 0)) for s in z]
+
+ # another sequence to swap values from
+ z = nx.utils.powerlaw_sequence(tries, exponent=gamma, seed=seed)
+ # round to integer values in the range [0,n]
+ swap = [min(n, max(round(s), 0)) for s in z]
+
+ for deg in swap:
+ # If this degree sequence can be the degree sequence of a tree, return
+ # it. It can be a tree if the number of edges is one fewer than the
+ # number of nodes, or in other words, `n - sum(zseq) / 2 == 1`. We
+ # use an equivalent condition below that avoids floating point
+ # operations.
+ if 2 * n - sum(zseq) == 2:
+ return zseq
+ index = seed.randint(0, n - 1)
+ zseq[index] = swap.pop()
+
+ raise nx.NetworkXError(
+ f"Exceeded max ({tries}) attempts for a valid tree sequence."
+ )
+
+
+@py_random_state(3)
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_kernel_graph(
+ n, kernel_integral, kernel_root=None, seed=None, *, create_using=None
+):
+ r"""Returns an random graph based on the specified kernel.
+
+ The algorithm chooses each of the $[n(n-1)]/2$ possible edges with
+ probability specified by a kernel $\kappa(x,y)$ [1]_. The kernel
+ $\kappa(x,y)$ must be a symmetric (in $x,y$), non-negative,
+ bounded function.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes
+ kernel_integral : function
+ Function that returns the definite integral of the kernel $\kappa(x,y)$,
+ $F(y,a,b) := \int_a^b \kappa(x,y)dx$
+ kernel_root: function (optional)
+ Function that returns the root $b$ of the equation $F(y,a,b) = r$.
+ If None, the root is found using :func:`scipy.optimize.brentq`
+ (this requires SciPy).
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ create_using : Graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ Multigraph and directed types are not supported and raise a ``NetworkXError``.
+
+ Notes
+ -----
+ The kernel is specified through its definite integral which must be
+ provided as one of the arguments. If the integral and root of the
+ kernel integral can be found in $O(1)$ time then this algorithm runs in
+ time $O(n+m)$ where m is the expected number of edges [2]_.
+
+ The nodes are set to integers from $0$ to $n-1$.
+
+ Examples
+ --------
+ Generate an Erdős–Rényi random graph $G(n,c/n)$, with kernel
+ $\kappa(x,y)=c$ where $c$ is the mean expected degree.
+
+ >>> def integral(u, w, z):
+ ... return c * (z - w)
+ >>> def root(u, w, r):
+ ... return r / c + w
+ >>> c = 1
+ >>> graph = nx.random_kernel_graph(1000, integral, root)
+
+ See Also
+ --------
+ gnp_random_graph
+ expected_degree_graph
+
+ References
+ ----------
+ .. [1] Bollobás, Béla, Janson, S. and Riordan, O.
+ "The phase transition in inhomogeneous random graphs",
+ *Random Structures Algorithms*, 31, 3--122, 2007.
+
+ .. [2] Hagberg A, Lemons N (2015),
+ "Fast Generation of Sparse Random Kernel Graphs".
+ PLoS ONE 10(9): e0135177, 2015. doi:10.1371/journal.pone.0135177
+ """
+ create_using = check_create_using(create_using, directed=False, multigraph=False)
+ if kernel_root is None:
+ import scipy as sp
+
+ def kernel_root(y, a, r):
+ def my_function(b):
+ return kernel_integral(y, a, b) - r
+
+ return sp.optimize.brentq(my_function, a, 1)
+
+ graph = nx.empty_graph(create_using=create_using)
+ graph.add_nodes_from(range(n))
+ (i, j) = (1, 1)
+ while i < n:
+ r = -math.log(1 - seed.random()) # (1-seed.random()) in (0, 1]
+ if kernel_integral(i / n, j / n, 1) <= r:
+ i, j = i + 1, i + 1
+ else:
+ j = math.ceil(n * kernel_root(i / n, j / n, r))
+ graph.add_edge(i - 1, j - 1)
+ return graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/small.py b/.venv/lib/python3.12/site-packages/networkx/generators/small.py
new file mode 100644
index 00000000..acd2fbc7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/small.py
@@ -0,0 +1,993 @@
+"""
+Various small and named graphs, together with some compact generators.
+
+"""
+
+__all__ = [
+ "LCF_graph",
+ "bull_graph",
+ "chvatal_graph",
+ "cubical_graph",
+ "desargues_graph",
+ "diamond_graph",
+ "dodecahedral_graph",
+ "frucht_graph",
+ "heawood_graph",
+ "hoffman_singleton_graph",
+ "house_graph",
+ "house_x_graph",
+ "icosahedral_graph",
+ "krackhardt_kite_graph",
+ "moebius_kantor_graph",
+ "octahedral_graph",
+ "pappus_graph",
+ "petersen_graph",
+ "sedgewick_maze_graph",
+ "tetrahedral_graph",
+ "truncated_cube_graph",
+ "truncated_tetrahedron_graph",
+ "tutte_graph",
+]
+
+from functools import wraps
+
+import networkx as nx
+from networkx.exception import NetworkXError
+from networkx.generators.classic import (
+ complete_graph,
+ cycle_graph,
+ empty_graph,
+ path_graph,
+)
+
+
+def _raise_on_directed(func):
+ """
+ A decorator which inspects the `create_using` argument and raises a
+ NetworkX exception when `create_using` is a DiGraph (class or instance) for
+ graph generators that do not support directed outputs.
+ """
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ if kwargs.get("create_using") is not None:
+ G = nx.empty_graph(create_using=kwargs["create_using"])
+ if G.is_directed():
+ raise NetworkXError("Directed Graph not supported")
+ return func(*args, **kwargs)
+
+ return wrapper
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def LCF_graph(n, shift_list, repeats, create_using=None):
+ """
+ Return the cubic graph specified in LCF notation.
+
+ LCF (Lederberg-Coxeter-Fruchte) notation[1]_ is a compressed
+ notation used in the generation of various cubic Hamiltonian
+ graphs of high symmetry. See, for example, `dodecahedral_graph`,
+ `desargues_graph`, `heawood_graph` and `pappus_graph`.
+
+ Nodes are drawn from ``range(n)``. Each node ``n_i`` is connected with
+ node ``n_i + shift % n`` where ``shift`` is given by cycling through
+ the input `shift_list` `repeat` s times.
+
+ Parameters
+ ----------
+ n : int
+ The starting graph is the `n`-cycle with nodes ``0, ..., n-1``.
+ The null graph is returned if `n` < 1.
+
+ shift_list : list
+ A list of integer shifts mod `n`, ``[s1, s2, .., sk]``
+
+ repeats : int
+ Integer specifying the number of times that shifts in `shift_list`
+ are successively applied to each current node in the n-cycle
+ to generate an edge between ``n_current`` and ``n_current + shift mod n``.
+
+ Returns
+ -------
+ G : Graph
+ A graph instance created from the specified LCF notation.
+
+ Examples
+ --------
+ The utility graph $K_{3,3}$
+
+ >>> G = nx.LCF_graph(6, [3, -3], 3)
+ >>> G.edges()
+ EdgeView([(0, 1), (0, 5), (0, 3), (1, 2), (1, 4), (2, 3), (2, 5), (3, 4), (4, 5)])
+
+ The Heawood graph:
+
+ >>> G = nx.LCF_graph(14, [5, -5], 7)
+ >>> nx.is_isomorphic(G, nx.heawood_graph())
+ True
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/LCF_notation
+
+ """
+ if n <= 0:
+ return empty_graph(0, create_using)
+
+ # start with the n-cycle
+ G = cycle_graph(n, create_using)
+ if G.is_directed():
+ raise NetworkXError("Directed Graph not supported")
+ G.name = "LCF_graph"
+ nodes = sorted(G)
+
+ n_extra_edges = repeats * len(shift_list)
+ # edges are added n_extra_edges times
+ # (not all of these need be new)
+ if n_extra_edges < 1:
+ return G
+
+ for i in range(n_extra_edges):
+ shift = shift_list[i % len(shift_list)] # cycle through shift_list
+ v1 = nodes[i % n] # cycle repeatedly through nodes
+ v2 = nodes[(i + shift) % n]
+ G.add_edge(v1, v2)
+ return G
+
+
+# -------------------------------------------------------------------------------
+# Various small and named graphs
+# -------------------------------------------------------------------------------
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def bull_graph(create_using=None):
+ """
+ Returns the Bull Graph
+
+ The Bull Graph has 5 nodes and 5 edges. It is a planar undirected
+ graph in the form of a triangle with two disjoint pendant edges [1]_
+ The name comes from the triangle and pendant edges representing
+ respectively the body and legs of a bull.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ A bull graph with 5 nodes
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Bull_graph.
+
+ """
+ G = nx.from_dict_of_lists(
+ {0: [1, 2], 1: [0, 2, 3], 2: [0, 1, 4], 3: [1], 4: [2]},
+ create_using=create_using,
+ )
+ G.name = "Bull Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def chvatal_graph(create_using=None):
+ """
+ Returns the Chvátal Graph
+
+ The Chvátal Graph is an undirected graph with 12 nodes and 24 edges [1]_.
+ It has 370 distinct (directed) Hamiltonian cycles, giving a unique generalized
+ LCF notation of order 4, two of order 6 , and 43 of order 1 [2]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ The Chvátal graph with 12 nodes and 24 edges
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Chv%C3%A1tal_graph
+ .. [2] https://mathworld.wolfram.com/ChvatalGraph.html
+
+ """
+ G = nx.from_dict_of_lists(
+ {
+ 0: [1, 4, 6, 9],
+ 1: [2, 5, 7],
+ 2: [3, 6, 8],
+ 3: [4, 7, 9],
+ 4: [5, 8],
+ 5: [10, 11],
+ 6: [10, 11],
+ 7: [8, 11],
+ 8: [10],
+ 9: [10, 11],
+ },
+ create_using=create_using,
+ )
+ G.name = "Chvatal Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def cubical_graph(create_using=None):
+ """
+ Returns the 3-regular Platonic Cubical Graph
+
+ The skeleton of the cube (the nodes and edges) form a graph, with 8
+ nodes, and 12 edges. It is a special case of the hypercube graph.
+ It is one of 5 Platonic graphs, each a skeleton of its
+ Platonic solid [1]_.
+ Such graphs arise in parallel processing in computers.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ A cubical graph with 8 nodes and 12 edges
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Cube#Cubical_graph
+
+ """
+ G = nx.from_dict_of_lists(
+ {
+ 0: [1, 3, 4],
+ 1: [0, 2, 7],
+ 2: [1, 3, 6],
+ 3: [0, 2, 5],
+ 4: [0, 5, 7],
+ 5: [3, 4, 6],
+ 6: [2, 5, 7],
+ 7: [1, 4, 6],
+ },
+ create_using=create_using,
+ )
+ G.name = "Platonic Cubical Graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def desargues_graph(create_using=None):
+ """
+ Returns the Desargues Graph
+
+ The Desargues Graph is a non-planar, distance-transitive cubic graph
+ with 20 nodes and 30 edges [1]_.
+ It is a symmetric graph. It can be represented in LCF notation
+ as [5,-5,9,-9]^5 [2]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Desargues Graph with 20 nodes and 30 edges
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Desargues_graph
+ .. [2] https://mathworld.wolfram.com/DesarguesGraph.html
+ """
+ G = LCF_graph(20, [5, -5, 9, -9], 5, create_using)
+ G.name = "Desargues Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def diamond_graph(create_using=None):
+ """
+ Returns the Diamond graph
+
+ The Diamond Graph is planar undirected graph with 4 nodes and 5 edges.
+ It is also sometimes known as the double triangle graph or kite graph [1]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Diamond Graph with 4 nodes and 5 edges
+
+ References
+ ----------
+ .. [1] https://mathworld.wolfram.com/DiamondGraph.html
+ """
+ G = nx.from_dict_of_lists(
+ {0: [1, 2], 1: [0, 2, 3], 2: [0, 1, 3], 3: [1, 2]}, create_using=create_using
+ )
+ G.name = "Diamond Graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def dodecahedral_graph(create_using=None):
+ """
+ Returns the Platonic Dodecahedral graph.
+
+ The dodecahedral graph has 20 nodes and 30 edges. The skeleton of the
+ dodecahedron forms a graph. It is one of 5 Platonic graphs [1]_.
+ It can be described in LCF notation as:
+ ``[10, 7, 4, -4, -7, 10, -4, 7, -7, 4]^2`` [2]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Dodecahedral Graph with 20 nodes and 30 edges
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Regular_dodecahedron#Dodecahedral_graph
+ .. [2] https://mathworld.wolfram.com/DodecahedralGraph.html
+
+ """
+ G = LCF_graph(20, [10, 7, 4, -4, -7, 10, -4, 7, -7, 4], 2, create_using)
+ G.name = "Dodecahedral Graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def frucht_graph(create_using=None):
+ """
+ Returns the Frucht Graph.
+
+ The Frucht Graph is the smallest cubical graph whose
+ automorphism group consists only of the identity element [1]_.
+ It has 12 nodes and 18 edges and no nontrivial symmetries.
+ It is planar and Hamiltonian [2]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Frucht Graph with 12 nodes and 18 edges
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Frucht_graph
+ .. [2] https://mathworld.wolfram.com/FruchtGraph.html
+
+ """
+ G = cycle_graph(7, create_using)
+ G.add_edges_from(
+ [
+ [0, 7],
+ [1, 7],
+ [2, 8],
+ [3, 9],
+ [4, 9],
+ [5, 10],
+ [6, 10],
+ [7, 11],
+ [8, 11],
+ [8, 9],
+ [10, 11],
+ ]
+ )
+
+ G.name = "Frucht Graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def heawood_graph(create_using=None):
+ """
+ Returns the Heawood Graph, a (3,6) cage.
+
+ The Heawood Graph is an undirected graph with 14 nodes and 21 edges,
+ named after Percy John Heawood [1]_.
+ It is cubic symmetric, nonplanar, Hamiltonian, and can be represented
+ in LCF notation as ``[5,-5]^7`` [2]_.
+ It is the unique (3,6)-cage: the regular cubic graph of girth 6 with
+ minimal number of vertices [3]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Heawood Graph with 14 nodes and 21 edges
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Heawood_graph
+ .. [2] https://mathworld.wolfram.com/HeawoodGraph.html
+ .. [3] https://www.win.tue.nl/~aeb/graphs/Heawood.html
+
+ """
+ G = LCF_graph(14, [5, -5], 7, create_using)
+ G.name = "Heawood Graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def hoffman_singleton_graph():
+ """
+ Returns the Hoffman-Singleton Graph.
+
+ The Hoffman–Singleton graph is a symmetrical undirected graph
+ with 50 nodes and 175 edges.
+ All indices lie in ``Z % 5``: that is, the integers mod 5 [1]_.
+ It is the only regular graph of vertex degree 7, diameter 2, and girth 5.
+ It is the unique (7,5)-cage graph and Moore graph, and contains many
+ copies of the Petersen graph [2]_.
+
+ Returns
+ -------
+ G : networkx Graph
+ Hoffman–Singleton Graph with 50 nodes and 175 edges
+
+ Notes
+ -----
+ Constructed from pentagon and pentagram as follows: Take five pentagons $P_h$
+ and five pentagrams $Q_i$ . Join vertex $j$ of $P_h$ to vertex $h·i+j$ of $Q_i$ [3]_.
+
+ References
+ ----------
+ .. [1] https://blogs.ams.org/visualinsight/2016/02/01/hoffman-singleton-graph/
+ .. [2] https://mathworld.wolfram.com/Hoffman-SingletonGraph.html
+ .. [3] https://en.wikipedia.org/wiki/Hoffman%E2%80%93Singleton_graph
+
+ """
+ G = nx.Graph()
+ for i in range(5):
+ for j in range(5):
+ G.add_edge(("pentagon", i, j), ("pentagon", i, (j - 1) % 5))
+ G.add_edge(("pentagon", i, j), ("pentagon", i, (j + 1) % 5))
+ G.add_edge(("pentagram", i, j), ("pentagram", i, (j - 2) % 5))
+ G.add_edge(("pentagram", i, j), ("pentagram", i, (j + 2) % 5))
+ for k in range(5):
+ G.add_edge(("pentagon", i, j), ("pentagram", k, (i * k + j) % 5))
+ G = nx.convert_node_labels_to_integers(G)
+ G.name = "Hoffman-Singleton Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def house_graph(create_using=None):
+ """
+ Returns the House graph (square with triangle on top)
+
+ The house graph is a simple undirected graph with
+ 5 nodes and 6 edges [1]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ House graph in the form of a square with a triangle on top
+
+ References
+ ----------
+ .. [1] https://mathworld.wolfram.com/HouseGraph.html
+ """
+ G = nx.from_dict_of_lists(
+ {0: [1, 2], 1: [0, 3], 2: [0, 3, 4], 3: [1, 2, 4], 4: [2, 3]},
+ create_using=create_using,
+ )
+ G.name = "House Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def house_x_graph(create_using=None):
+ """
+ Returns the House graph with a cross inside the house square.
+
+ The House X-graph is the House graph plus the two edges connecting diagonally
+ opposite vertices of the square base. It is also one of the two graphs
+ obtained by removing two edges from the pentatope graph [1]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ House graph with diagonal vertices connected
+
+ References
+ ----------
+ .. [1] https://mathworld.wolfram.com/HouseGraph.html
+ """
+ G = house_graph(create_using)
+ G.add_edges_from([(0, 3), (1, 2)])
+ G.name = "House-with-X-inside Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def icosahedral_graph(create_using=None):
+ """
+ Returns the Platonic Icosahedral graph.
+
+ The icosahedral graph has 12 nodes and 30 edges. It is a Platonic graph
+ whose nodes have the connectivity of the icosahedron. It is undirected,
+ regular and Hamiltonian [1]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Icosahedral graph with 12 nodes and 30 edges.
+
+ References
+ ----------
+ .. [1] https://mathworld.wolfram.com/IcosahedralGraph.html
+ """
+ G = nx.from_dict_of_lists(
+ {
+ 0: [1, 5, 7, 8, 11],
+ 1: [2, 5, 6, 8],
+ 2: [3, 6, 8, 9],
+ 3: [4, 6, 9, 10],
+ 4: [5, 6, 10, 11],
+ 5: [6, 11],
+ 7: [8, 9, 10, 11],
+ 8: [9],
+ 9: [10],
+ 10: [11],
+ },
+ create_using=create_using,
+ )
+ G.name = "Platonic Icosahedral Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def krackhardt_kite_graph(create_using=None):
+ """
+ Returns the Krackhardt Kite Social Network.
+
+ A 10 actor social network introduced by David Krackhardt
+ to illustrate different centrality measures [1]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Krackhardt Kite graph with 10 nodes and 18 edges
+
+ Notes
+ -----
+ The traditional labeling is:
+ Andre=1, Beverley=2, Carol=3, Diane=4,
+ Ed=5, Fernando=6, Garth=7, Heather=8, Ike=9, Jane=10.
+
+ References
+ ----------
+ .. [1] Krackhardt, David. "Assessing the Political Landscape: Structure,
+ Cognition, and Power in Organizations". Administrative Science Quarterly.
+ 35 (2): 342–369. doi:10.2307/2393394. JSTOR 2393394. June 1990.
+
+ """
+ G = nx.from_dict_of_lists(
+ {
+ 0: [1, 2, 3, 5],
+ 1: [0, 3, 4, 6],
+ 2: [0, 3, 5],
+ 3: [0, 1, 2, 4, 5, 6],
+ 4: [1, 3, 6],
+ 5: [0, 2, 3, 6, 7],
+ 6: [1, 3, 4, 5, 7],
+ 7: [5, 6, 8],
+ 8: [7, 9],
+ 9: [8],
+ },
+ create_using=create_using,
+ )
+ G.name = "Krackhardt Kite Social Network"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def moebius_kantor_graph(create_using=None):
+ """
+ Returns the Moebius-Kantor graph.
+
+ The Möbius-Kantor graph is the cubic symmetric graph on 16 nodes.
+ Its LCF notation is [5,-5]^8, and it is isomorphic to the generalized
+ Petersen graph [1]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Moebius-Kantor graph
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/M%C3%B6bius%E2%80%93Kantor_graph
+
+ """
+ G = LCF_graph(16, [5, -5], 8, create_using)
+ G.name = "Moebius-Kantor Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def octahedral_graph(create_using=None):
+ """
+ Returns the Platonic Octahedral graph.
+
+ The octahedral graph is the 6-node 12-edge Platonic graph having the
+ connectivity of the octahedron [1]_. If 6 couples go to a party,
+ and each person shakes hands with every person except his or her partner,
+ then this graph describes the set of handshakes that take place;
+ for this reason it is also called the cocktail party graph [2]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Octahedral graph
+
+ References
+ ----------
+ .. [1] https://mathworld.wolfram.com/OctahedralGraph.html
+ .. [2] https://en.wikipedia.org/wiki/Tur%C3%A1n_graph#Special_cases
+
+ """
+ G = nx.from_dict_of_lists(
+ {0: [1, 2, 3, 4], 1: [2, 3, 5], 2: [4, 5], 3: [4, 5], 4: [5]},
+ create_using=create_using,
+ )
+ G.name = "Platonic Octahedral Graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def pappus_graph():
+ """
+ Returns the Pappus graph.
+
+ The Pappus graph is a cubic symmetric distance-regular graph with 18 nodes
+ and 27 edges. It is Hamiltonian and can be represented in LCF notation as
+ [5,7,-7,7,-7,-5]^3 [1]_.
+
+ Returns
+ -------
+ G : networkx Graph
+ Pappus graph
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Pappus_graph
+ """
+ G = LCF_graph(18, [5, 7, -7, 7, -7, -5], 3)
+ G.name = "Pappus Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def petersen_graph(create_using=None):
+ """
+ Returns the Petersen graph.
+
+ The Peterson graph is a cubic, undirected graph with 10 nodes and 15 edges [1]_.
+ Julius Petersen constructed the graph as the smallest counterexample
+ against the claim that a connected bridgeless cubic graph
+ has an edge colouring with three colours [2]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Petersen graph
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Petersen_graph
+ .. [2] https://www.win.tue.nl/~aeb/drg/graphs/Petersen.html
+ """
+ G = nx.from_dict_of_lists(
+ {
+ 0: [1, 4, 5],
+ 1: [0, 2, 6],
+ 2: [1, 3, 7],
+ 3: [2, 4, 8],
+ 4: [3, 0, 9],
+ 5: [0, 7, 8],
+ 6: [1, 8, 9],
+ 7: [2, 5, 9],
+ 8: [3, 5, 6],
+ 9: [4, 6, 7],
+ },
+ create_using=create_using,
+ )
+ G.name = "Petersen Graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def sedgewick_maze_graph(create_using=None):
+ """
+ Return a small maze with a cycle.
+
+ This is the maze used in Sedgewick, 3rd Edition, Part 5, Graph
+ Algorithms, Chapter 18, e.g. Figure 18.2 and following [1]_.
+ Nodes are numbered 0,..,7
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Small maze with a cycle
+
+ References
+ ----------
+ .. [1] Figure 18.2, Chapter 18, Graph Algorithms (3rd Ed), Sedgewick
+ """
+ G = empty_graph(0, create_using)
+ G.add_nodes_from(range(8))
+ G.add_edges_from([[0, 2], [0, 7], [0, 5]])
+ G.add_edges_from([[1, 7], [2, 6]])
+ G.add_edges_from([[3, 4], [3, 5]])
+ G.add_edges_from([[4, 5], [4, 7], [4, 6]])
+ G.name = "Sedgewick Maze"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def tetrahedral_graph(create_using=None):
+ """
+ Returns the 3-regular Platonic Tetrahedral graph.
+
+ Tetrahedral graph has 4 nodes and 6 edges. It is a
+ special case of the complete graph, K4, and wheel graph, W4.
+ It is one of the 5 platonic graphs [1]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Tetrahedral Graph
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Tetrahedron#Tetrahedral_graph
+
+ """
+ G = complete_graph(4, create_using)
+ G.name = "Platonic Tetrahedral Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def truncated_cube_graph(create_using=None):
+ """
+ Returns the skeleton of the truncated cube.
+
+ The truncated cube is an Archimedean solid with 14 regular
+ faces (6 octagonal and 8 triangular), 36 edges and 24 nodes [1]_.
+ The truncated cube is created by truncating (cutting off) the tips
+ of the cube one third of the way into each edge [2]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Skeleton of the truncated cube
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Truncated_cube
+ .. [2] https://www.coolmath.com/reference/polyhedra-truncated-cube
+
+ """
+ G = nx.from_dict_of_lists(
+ {
+ 0: [1, 2, 4],
+ 1: [11, 14],
+ 2: [3, 4],
+ 3: [6, 8],
+ 4: [5],
+ 5: [16, 18],
+ 6: [7, 8],
+ 7: [10, 12],
+ 8: [9],
+ 9: [17, 20],
+ 10: [11, 12],
+ 11: [14],
+ 12: [13],
+ 13: [21, 22],
+ 14: [15],
+ 15: [19, 23],
+ 16: [17, 18],
+ 17: [20],
+ 18: [19],
+ 19: [23],
+ 20: [21],
+ 21: [22],
+ 22: [23],
+ },
+ create_using=create_using,
+ )
+ G.name = "Truncated Cube Graph"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def truncated_tetrahedron_graph(create_using=None):
+ """
+ Returns the skeleton of the truncated Platonic tetrahedron.
+
+ The truncated tetrahedron is an Archimedean solid with 4 regular hexagonal faces,
+ 4 equilateral triangle faces, 12 nodes and 18 edges. It can be constructed by truncating
+ all 4 vertices of a regular tetrahedron at one third of the original edge length [1]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Skeleton of the truncated tetrahedron
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Truncated_tetrahedron
+
+ """
+ G = path_graph(12, create_using)
+ G.add_edges_from([(0, 2), (0, 9), (1, 6), (3, 11), (4, 11), (5, 7), (8, 10)])
+ G.name = "Truncated Tetrahedron Graph"
+ return G
+
+
+@_raise_on_directed
+@nx._dispatchable(graphs=None, returns_graph=True)
+def tutte_graph(create_using=None):
+ """
+ Returns the Tutte graph.
+
+ The Tutte graph is a cubic polyhedral, non-Hamiltonian graph. It has
+ 46 nodes and 69 edges.
+ It is a counterexample to Tait's conjecture that every 3-regular polyhedron
+ has a Hamiltonian cycle.
+ It can be realized geometrically from a tetrahedron by multiply truncating
+ three of its vertices [1]_.
+
+ Parameters
+ ----------
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ Returns
+ -------
+ G : networkx Graph
+ Tutte graph
+
+ References
+ ----------
+ .. [1] https://en.wikipedia.org/wiki/Tutte_graph
+ """
+ G = nx.from_dict_of_lists(
+ {
+ 0: [1, 2, 3],
+ 1: [4, 26],
+ 2: [10, 11],
+ 3: [18, 19],
+ 4: [5, 33],
+ 5: [6, 29],
+ 6: [7, 27],
+ 7: [8, 14],
+ 8: [9, 38],
+ 9: [10, 37],
+ 10: [39],
+ 11: [12, 39],
+ 12: [13, 35],
+ 13: [14, 15],
+ 14: [34],
+ 15: [16, 22],
+ 16: [17, 44],
+ 17: [18, 43],
+ 18: [45],
+ 19: [20, 45],
+ 20: [21, 41],
+ 21: [22, 23],
+ 22: [40],
+ 23: [24, 27],
+ 24: [25, 32],
+ 25: [26, 31],
+ 26: [33],
+ 27: [28],
+ 28: [29, 32],
+ 29: [30],
+ 30: [31, 33],
+ 31: [32],
+ 34: [35, 38],
+ 35: [36],
+ 36: [37, 39],
+ 37: [38],
+ 40: [41, 44],
+ 41: [42],
+ 42: [43, 45],
+ 43: [44],
+ },
+ create_using=create_using,
+ )
+ G.name = "Tutte's Graph"
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/social.py b/.venv/lib/python3.12/site-packages/networkx/generators/social.py
new file mode 100644
index 00000000..f41b2d88
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/social.py
@@ -0,0 +1,554 @@
+"""
+Famous social networks.
+"""
+
+import networkx as nx
+
+__all__ = [
+ "karate_club_graph",
+ "davis_southern_women_graph",
+ "florentine_families_graph",
+ "les_miserables_graph",
+]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def karate_club_graph():
+ """Returns Zachary's Karate Club graph.
+
+ Each node in the returned graph has a node attribute 'club' that
+ indicates the name of the club to which the member represented by that node
+ belongs, either 'Mr. Hi' or 'Officer'. Each edge has a weight based on the
+ number of contexts in which that edge's incident node members interacted.
+
+ The dataset is derived from the 'Club After Split From Data' column of Table 3 in [1]_.
+ This was in turn derived from the 'Club After Fission' column of Table 1 in the
+ same paper. Note that the nodes are 0-indexed in NetworkX, but 1-indexed in the
+ paper (the 'Individual Number in Matrix C' column of Table 3 starts at 1). This
+ means, for example, that ``G.nodes[9]["club"]`` returns 'Officer', which
+ corresponds to row 10 of Table 3 in the paper.
+
+ Examples
+ --------
+ To get the name of the club to which a node belongs::
+
+ >>> G = nx.karate_club_graph()
+ >>> G.nodes[5]["club"]
+ 'Mr. Hi'
+ >>> G.nodes[9]["club"]
+ 'Officer'
+
+ References
+ ----------
+ .. [1] Zachary, Wayne W.
+ "An Information Flow Model for Conflict and Fission in Small Groups."
+ *Journal of Anthropological Research*, 33, 452--473, (1977).
+ """
+ # Create the set of all members, and the members of each club.
+ all_members = set(range(34))
+ club1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21}
+ # club2 = all_members - club1
+
+ G = nx.Graph()
+ G.add_nodes_from(all_members)
+ G.name = "Zachary's Karate Club"
+
+ zacharydat = """\
+0 4 5 3 3 3 3 2 2 0 2 3 2 3 0 0 0 2 0 2 0 2 0 0 0 0 0 0 0 0 0 2 0 0
+4 0 6 3 0 0 0 4 0 0 0 0 0 5 0 0 0 1 0 2 0 2 0 0 0 0 0 0 0 0 2 0 0 0
+5 6 0 3 0 0 0 4 5 1 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 0 0 0 3 0
+3 3 3 0 0 0 0 3 0 0 0 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+3 0 0 0 0 0 2 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+3 0 0 0 0 0 5 0 0 0 3 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+3 0 0 0 2 5 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+2 4 4 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+2 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 4 3
+0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2
+2 0 0 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+1 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+3 5 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 2
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 4
+0 0 0 0 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2
+2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 1
+2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0 4 0 2 0 0 5 4
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 3 0 0 0 2 0 0
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 2 0 0 0 0 0 0 7 0 0
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 2
+0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 3 0 0 0 0 0 0 0 0 4
+0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 2
+0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 4 0 0 0 0 0 3 2
+0 2 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 3
+2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 7 0 0 2 0 0 0 4 4
+0 0 2 0 0 0 0 0 3 0 0 0 0 0 3 3 0 0 1 0 3 0 2 5 0 0 0 0 0 4 3 4 0 5
+0 0 0 0 0 0 0 0 4 2 0 0 0 3 2 4 0 0 2 1 1 0 3 4 0 0 2 4 2 2 3 4 5 0"""
+
+ for row, line in enumerate(zacharydat.split("\n")):
+ thisrow = [int(b) for b in line.split()]
+ for col, entry in enumerate(thisrow):
+ if entry >= 1:
+ G.add_edge(row, col, weight=entry)
+
+ # Add the name of each member's club as a node attribute.
+ for v in G:
+ G.nodes[v]["club"] = "Mr. Hi" if v in club1 else "Officer"
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def davis_southern_women_graph():
+ """Returns Davis Southern women social network.
+
+ This is a bipartite graph.
+
+ References
+ ----------
+ .. [1] A. Davis, Gardner, B. B., Gardner, M. R., 1941. Deep South.
+ University of Chicago Press, Chicago, IL.
+ """
+ G = nx.Graph()
+ # Top nodes
+ women = [
+ "Evelyn Jefferson",
+ "Laura Mandeville",
+ "Theresa Anderson",
+ "Brenda Rogers",
+ "Charlotte McDowd",
+ "Frances Anderson",
+ "Eleanor Nye",
+ "Pearl Oglethorpe",
+ "Ruth DeSand",
+ "Verne Sanderson",
+ "Myra Liddel",
+ "Katherina Rogers",
+ "Sylvia Avondale",
+ "Nora Fayette",
+ "Helen Lloyd",
+ "Dorothy Murchison",
+ "Olivia Carleton",
+ "Flora Price",
+ ]
+ G.add_nodes_from(women, bipartite=0)
+ # Bottom nodes
+ events = [
+ "E1",
+ "E2",
+ "E3",
+ "E4",
+ "E5",
+ "E6",
+ "E7",
+ "E8",
+ "E9",
+ "E10",
+ "E11",
+ "E12",
+ "E13",
+ "E14",
+ ]
+ G.add_nodes_from(events, bipartite=1)
+
+ G.add_edges_from(
+ [
+ ("Evelyn Jefferson", "E1"),
+ ("Evelyn Jefferson", "E2"),
+ ("Evelyn Jefferson", "E3"),
+ ("Evelyn Jefferson", "E4"),
+ ("Evelyn Jefferson", "E5"),
+ ("Evelyn Jefferson", "E6"),
+ ("Evelyn Jefferson", "E8"),
+ ("Evelyn Jefferson", "E9"),
+ ("Laura Mandeville", "E1"),
+ ("Laura Mandeville", "E2"),
+ ("Laura Mandeville", "E3"),
+ ("Laura Mandeville", "E5"),
+ ("Laura Mandeville", "E6"),
+ ("Laura Mandeville", "E7"),
+ ("Laura Mandeville", "E8"),
+ ("Theresa Anderson", "E2"),
+ ("Theresa Anderson", "E3"),
+ ("Theresa Anderson", "E4"),
+ ("Theresa Anderson", "E5"),
+ ("Theresa Anderson", "E6"),
+ ("Theresa Anderson", "E7"),
+ ("Theresa Anderson", "E8"),
+ ("Theresa Anderson", "E9"),
+ ("Brenda Rogers", "E1"),
+ ("Brenda Rogers", "E3"),
+ ("Brenda Rogers", "E4"),
+ ("Brenda Rogers", "E5"),
+ ("Brenda Rogers", "E6"),
+ ("Brenda Rogers", "E7"),
+ ("Brenda Rogers", "E8"),
+ ("Charlotte McDowd", "E3"),
+ ("Charlotte McDowd", "E4"),
+ ("Charlotte McDowd", "E5"),
+ ("Charlotte McDowd", "E7"),
+ ("Frances Anderson", "E3"),
+ ("Frances Anderson", "E5"),
+ ("Frances Anderson", "E6"),
+ ("Frances Anderson", "E8"),
+ ("Eleanor Nye", "E5"),
+ ("Eleanor Nye", "E6"),
+ ("Eleanor Nye", "E7"),
+ ("Eleanor Nye", "E8"),
+ ("Pearl Oglethorpe", "E6"),
+ ("Pearl Oglethorpe", "E8"),
+ ("Pearl Oglethorpe", "E9"),
+ ("Ruth DeSand", "E5"),
+ ("Ruth DeSand", "E7"),
+ ("Ruth DeSand", "E8"),
+ ("Ruth DeSand", "E9"),
+ ("Verne Sanderson", "E7"),
+ ("Verne Sanderson", "E8"),
+ ("Verne Sanderson", "E9"),
+ ("Verne Sanderson", "E12"),
+ ("Myra Liddel", "E8"),
+ ("Myra Liddel", "E9"),
+ ("Myra Liddel", "E10"),
+ ("Myra Liddel", "E12"),
+ ("Katherina Rogers", "E8"),
+ ("Katherina Rogers", "E9"),
+ ("Katherina Rogers", "E10"),
+ ("Katherina Rogers", "E12"),
+ ("Katherina Rogers", "E13"),
+ ("Katherina Rogers", "E14"),
+ ("Sylvia Avondale", "E7"),
+ ("Sylvia Avondale", "E8"),
+ ("Sylvia Avondale", "E9"),
+ ("Sylvia Avondale", "E10"),
+ ("Sylvia Avondale", "E12"),
+ ("Sylvia Avondale", "E13"),
+ ("Sylvia Avondale", "E14"),
+ ("Nora Fayette", "E6"),
+ ("Nora Fayette", "E7"),
+ ("Nora Fayette", "E9"),
+ ("Nora Fayette", "E10"),
+ ("Nora Fayette", "E11"),
+ ("Nora Fayette", "E12"),
+ ("Nora Fayette", "E13"),
+ ("Nora Fayette", "E14"),
+ ("Helen Lloyd", "E7"),
+ ("Helen Lloyd", "E8"),
+ ("Helen Lloyd", "E10"),
+ ("Helen Lloyd", "E11"),
+ ("Helen Lloyd", "E12"),
+ ("Dorothy Murchison", "E8"),
+ ("Dorothy Murchison", "E9"),
+ ("Olivia Carleton", "E9"),
+ ("Olivia Carleton", "E11"),
+ ("Flora Price", "E9"),
+ ("Flora Price", "E11"),
+ ]
+ )
+ G.graph["top"] = women
+ G.graph["bottom"] = events
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def florentine_families_graph():
+ """Returns Florentine families graph.
+
+ References
+ ----------
+ .. [1] Ronald L. Breiger and Philippa E. Pattison
+ Cumulated social roles: The duality of persons and their algebras,1
+ Social Networks, Volume 8, Issue 3, September 1986, Pages 215-256
+ """
+ G = nx.Graph()
+ G.add_edge("Acciaiuoli", "Medici")
+ G.add_edge("Castellani", "Peruzzi")
+ G.add_edge("Castellani", "Strozzi")
+ G.add_edge("Castellani", "Barbadori")
+ G.add_edge("Medici", "Barbadori")
+ G.add_edge("Medici", "Ridolfi")
+ G.add_edge("Medici", "Tornabuoni")
+ G.add_edge("Medici", "Albizzi")
+ G.add_edge("Medici", "Salviati")
+ G.add_edge("Salviati", "Pazzi")
+ G.add_edge("Peruzzi", "Strozzi")
+ G.add_edge("Peruzzi", "Bischeri")
+ G.add_edge("Strozzi", "Ridolfi")
+ G.add_edge("Strozzi", "Bischeri")
+ G.add_edge("Ridolfi", "Tornabuoni")
+ G.add_edge("Tornabuoni", "Guadagni")
+ G.add_edge("Albizzi", "Ginori")
+ G.add_edge("Albizzi", "Guadagni")
+ G.add_edge("Bischeri", "Guadagni")
+ G.add_edge("Guadagni", "Lamberteschi")
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def les_miserables_graph():
+ """Returns coappearance network of characters in the novel Les Miserables.
+
+ References
+ ----------
+ .. [1] D. E. Knuth, 1993.
+ The Stanford GraphBase: a platform for combinatorial computing,
+ pp. 74-87. New York: AcM Press.
+ """
+ G = nx.Graph()
+ G.add_edge("Napoleon", "Myriel", weight=1)
+ G.add_edge("MlleBaptistine", "Myriel", weight=8)
+ G.add_edge("MmeMagloire", "Myriel", weight=10)
+ G.add_edge("MmeMagloire", "MlleBaptistine", weight=6)
+ G.add_edge("CountessDeLo", "Myriel", weight=1)
+ G.add_edge("Geborand", "Myriel", weight=1)
+ G.add_edge("Champtercier", "Myriel", weight=1)
+ G.add_edge("Cravatte", "Myriel", weight=1)
+ G.add_edge("Count", "Myriel", weight=2)
+ G.add_edge("OldMan", "Myriel", weight=1)
+ G.add_edge("Valjean", "Labarre", weight=1)
+ G.add_edge("Valjean", "MmeMagloire", weight=3)
+ G.add_edge("Valjean", "MlleBaptistine", weight=3)
+ G.add_edge("Valjean", "Myriel", weight=5)
+ G.add_edge("Marguerite", "Valjean", weight=1)
+ G.add_edge("MmeDeR", "Valjean", weight=1)
+ G.add_edge("Isabeau", "Valjean", weight=1)
+ G.add_edge("Gervais", "Valjean", weight=1)
+ G.add_edge("Listolier", "Tholomyes", weight=4)
+ G.add_edge("Fameuil", "Tholomyes", weight=4)
+ G.add_edge("Fameuil", "Listolier", weight=4)
+ G.add_edge("Blacheville", "Tholomyes", weight=4)
+ G.add_edge("Blacheville", "Listolier", weight=4)
+ G.add_edge("Blacheville", "Fameuil", weight=4)
+ G.add_edge("Favourite", "Tholomyes", weight=3)
+ G.add_edge("Favourite", "Listolier", weight=3)
+ G.add_edge("Favourite", "Fameuil", weight=3)
+ G.add_edge("Favourite", "Blacheville", weight=4)
+ G.add_edge("Dahlia", "Tholomyes", weight=3)
+ G.add_edge("Dahlia", "Listolier", weight=3)
+ G.add_edge("Dahlia", "Fameuil", weight=3)
+ G.add_edge("Dahlia", "Blacheville", weight=3)
+ G.add_edge("Dahlia", "Favourite", weight=5)
+ G.add_edge("Zephine", "Tholomyes", weight=3)
+ G.add_edge("Zephine", "Listolier", weight=3)
+ G.add_edge("Zephine", "Fameuil", weight=3)
+ G.add_edge("Zephine", "Blacheville", weight=3)
+ G.add_edge("Zephine", "Favourite", weight=4)
+ G.add_edge("Zephine", "Dahlia", weight=4)
+ G.add_edge("Fantine", "Tholomyes", weight=3)
+ G.add_edge("Fantine", "Listolier", weight=3)
+ G.add_edge("Fantine", "Fameuil", weight=3)
+ G.add_edge("Fantine", "Blacheville", weight=3)
+ G.add_edge("Fantine", "Favourite", weight=4)
+ G.add_edge("Fantine", "Dahlia", weight=4)
+ G.add_edge("Fantine", "Zephine", weight=4)
+ G.add_edge("Fantine", "Marguerite", weight=2)
+ G.add_edge("Fantine", "Valjean", weight=9)
+ G.add_edge("MmeThenardier", "Fantine", weight=2)
+ G.add_edge("MmeThenardier", "Valjean", weight=7)
+ G.add_edge("Thenardier", "MmeThenardier", weight=13)
+ G.add_edge("Thenardier", "Fantine", weight=1)
+ G.add_edge("Thenardier", "Valjean", weight=12)
+ G.add_edge("Cosette", "MmeThenardier", weight=4)
+ G.add_edge("Cosette", "Valjean", weight=31)
+ G.add_edge("Cosette", "Tholomyes", weight=1)
+ G.add_edge("Cosette", "Thenardier", weight=1)
+ G.add_edge("Javert", "Valjean", weight=17)
+ G.add_edge("Javert", "Fantine", weight=5)
+ G.add_edge("Javert", "Thenardier", weight=5)
+ G.add_edge("Javert", "MmeThenardier", weight=1)
+ G.add_edge("Javert", "Cosette", weight=1)
+ G.add_edge("Fauchelevent", "Valjean", weight=8)
+ G.add_edge("Fauchelevent", "Javert", weight=1)
+ G.add_edge("Bamatabois", "Fantine", weight=1)
+ G.add_edge("Bamatabois", "Javert", weight=1)
+ G.add_edge("Bamatabois", "Valjean", weight=2)
+ G.add_edge("Perpetue", "Fantine", weight=1)
+ G.add_edge("Simplice", "Perpetue", weight=2)
+ G.add_edge("Simplice", "Valjean", weight=3)
+ G.add_edge("Simplice", "Fantine", weight=2)
+ G.add_edge("Simplice", "Javert", weight=1)
+ G.add_edge("Scaufflaire", "Valjean", weight=1)
+ G.add_edge("Woman1", "Valjean", weight=2)
+ G.add_edge("Woman1", "Javert", weight=1)
+ G.add_edge("Judge", "Valjean", weight=3)
+ G.add_edge("Judge", "Bamatabois", weight=2)
+ G.add_edge("Champmathieu", "Valjean", weight=3)
+ G.add_edge("Champmathieu", "Judge", weight=3)
+ G.add_edge("Champmathieu", "Bamatabois", weight=2)
+ G.add_edge("Brevet", "Judge", weight=2)
+ G.add_edge("Brevet", "Champmathieu", weight=2)
+ G.add_edge("Brevet", "Valjean", weight=2)
+ G.add_edge("Brevet", "Bamatabois", weight=1)
+ G.add_edge("Chenildieu", "Judge", weight=2)
+ G.add_edge("Chenildieu", "Champmathieu", weight=2)
+ G.add_edge("Chenildieu", "Brevet", weight=2)
+ G.add_edge("Chenildieu", "Valjean", weight=2)
+ G.add_edge("Chenildieu", "Bamatabois", weight=1)
+ G.add_edge("Cochepaille", "Judge", weight=2)
+ G.add_edge("Cochepaille", "Champmathieu", weight=2)
+ G.add_edge("Cochepaille", "Brevet", weight=2)
+ G.add_edge("Cochepaille", "Chenildieu", weight=2)
+ G.add_edge("Cochepaille", "Valjean", weight=2)
+ G.add_edge("Cochepaille", "Bamatabois", weight=1)
+ G.add_edge("Pontmercy", "Thenardier", weight=1)
+ G.add_edge("Boulatruelle", "Thenardier", weight=1)
+ G.add_edge("Eponine", "MmeThenardier", weight=2)
+ G.add_edge("Eponine", "Thenardier", weight=3)
+ G.add_edge("Anzelma", "Eponine", weight=2)
+ G.add_edge("Anzelma", "Thenardier", weight=2)
+ G.add_edge("Anzelma", "MmeThenardier", weight=1)
+ G.add_edge("Woman2", "Valjean", weight=3)
+ G.add_edge("Woman2", "Cosette", weight=1)
+ G.add_edge("Woman2", "Javert", weight=1)
+ G.add_edge("MotherInnocent", "Fauchelevent", weight=3)
+ G.add_edge("MotherInnocent", "Valjean", weight=1)
+ G.add_edge("Gribier", "Fauchelevent", weight=2)
+ G.add_edge("MmeBurgon", "Jondrette", weight=1)
+ G.add_edge("Gavroche", "MmeBurgon", weight=2)
+ G.add_edge("Gavroche", "Thenardier", weight=1)
+ G.add_edge("Gavroche", "Javert", weight=1)
+ G.add_edge("Gavroche", "Valjean", weight=1)
+ G.add_edge("Gillenormand", "Cosette", weight=3)
+ G.add_edge("Gillenormand", "Valjean", weight=2)
+ G.add_edge("Magnon", "Gillenormand", weight=1)
+ G.add_edge("Magnon", "MmeThenardier", weight=1)
+ G.add_edge("MlleGillenormand", "Gillenormand", weight=9)
+ G.add_edge("MlleGillenormand", "Cosette", weight=2)
+ G.add_edge("MlleGillenormand", "Valjean", weight=2)
+ G.add_edge("MmePontmercy", "MlleGillenormand", weight=1)
+ G.add_edge("MmePontmercy", "Pontmercy", weight=1)
+ G.add_edge("MlleVaubois", "MlleGillenormand", weight=1)
+ G.add_edge("LtGillenormand", "MlleGillenormand", weight=2)
+ G.add_edge("LtGillenormand", "Gillenormand", weight=1)
+ G.add_edge("LtGillenormand", "Cosette", weight=1)
+ G.add_edge("Marius", "MlleGillenormand", weight=6)
+ G.add_edge("Marius", "Gillenormand", weight=12)
+ G.add_edge("Marius", "Pontmercy", weight=1)
+ G.add_edge("Marius", "LtGillenormand", weight=1)
+ G.add_edge("Marius", "Cosette", weight=21)
+ G.add_edge("Marius", "Valjean", weight=19)
+ G.add_edge("Marius", "Tholomyes", weight=1)
+ G.add_edge("Marius", "Thenardier", weight=2)
+ G.add_edge("Marius", "Eponine", weight=5)
+ G.add_edge("Marius", "Gavroche", weight=4)
+ G.add_edge("BaronessT", "Gillenormand", weight=1)
+ G.add_edge("BaronessT", "Marius", weight=1)
+ G.add_edge("Mabeuf", "Marius", weight=1)
+ G.add_edge("Mabeuf", "Eponine", weight=1)
+ G.add_edge("Mabeuf", "Gavroche", weight=1)
+ G.add_edge("Enjolras", "Marius", weight=7)
+ G.add_edge("Enjolras", "Gavroche", weight=7)
+ G.add_edge("Enjolras", "Javert", weight=6)
+ G.add_edge("Enjolras", "Mabeuf", weight=1)
+ G.add_edge("Enjolras", "Valjean", weight=4)
+ G.add_edge("Combeferre", "Enjolras", weight=15)
+ G.add_edge("Combeferre", "Marius", weight=5)
+ G.add_edge("Combeferre", "Gavroche", weight=6)
+ G.add_edge("Combeferre", "Mabeuf", weight=2)
+ G.add_edge("Prouvaire", "Gavroche", weight=1)
+ G.add_edge("Prouvaire", "Enjolras", weight=4)
+ G.add_edge("Prouvaire", "Combeferre", weight=2)
+ G.add_edge("Feuilly", "Gavroche", weight=2)
+ G.add_edge("Feuilly", "Enjolras", weight=6)
+ G.add_edge("Feuilly", "Prouvaire", weight=2)
+ G.add_edge("Feuilly", "Combeferre", weight=5)
+ G.add_edge("Feuilly", "Mabeuf", weight=1)
+ G.add_edge("Feuilly", "Marius", weight=1)
+ G.add_edge("Courfeyrac", "Marius", weight=9)
+ G.add_edge("Courfeyrac", "Enjolras", weight=17)
+ G.add_edge("Courfeyrac", "Combeferre", weight=13)
+ G.add_edge("Courfeyrac", "Gavroche", weight=7)
+ G.add_edge("Courfeyrac", "Mabeuf", weight=2)
+ G.add_edge("Courfeyrac", "Eponine", weight=1)
+ G.add_edge("Courfeyrac", "Feuilly", weight=6)
+ G.add_edge("Courfeyrac", "Prouvaire", weight=3)
+ G.add_edge("Bahorel", "Combeferre", weight=5)
+ G.add_edge("Bahorel", "Gavroche", weight=5)
+ G.add_edge("Bahorel", "Courfeyrac", weight=6)
+ G.add_edge("Bahorel", "Mabeuf", weight=2)
+ G.add_edge("Bahorel", "Enjolras", weight=4)
+ G.add_edge("Bahorel", "Feuilly", weight=3)
+ G.add_edge("Bahorel", "Prouvaire", weight=2)
+ G.add_edge("Bahorel", "Marius", weight=1)
+ G.add_edge("Bossuet", "Marius", weight=5)
+ G.add_edge("Bossuet", "Courfeyrac", weight=12)
+ G.add_edge("Bossuet", "Gavroche", weight=5)
+ G.add_edge("Bossuet", "Bahorel", weight=4)
+ G.add_edge("Bossuet", "Enjolras", weight=10)
+ G.add_edge("Bossuet", "Feuilly", weight=6)
+ G.add_edge("Bossuet", "Prouvaire", weight=2)
+ G.add_edge("Bossuet", "Combeferre", weight=9)
+ G.add_edge("Bossuet", "Mabeuf", weight=1)
+ G.add_edge("Bossuet", "Valjean", weight=1)
+ G.add_edge("Joly", "Bahorel", weight=5)
+ G.add_edge("Joly", "Bossuet", weight=7)
+ G.add_edge("Joly", "Gavroche", weight=3)
+ G.add_edge("Joly", "Courfeyrac", weight=5)
+ G.add_edge("Joly", "Enjolras", weight=5)
+ G.add_edge("Joly", "Feuilly", weight=5)
+ G.add_edge("Joly", "Prouvaire", weight=2)
+ G.add_edge("Joly", "Combeferre", weight=5)
+ G.add_edge("Joly", "Mabeuf", weight=1)
+ G.add_edge("Joly", "Marius", weight=2)
+ G.add_edge("Grantaire", "Bossuet", weight=3)
+ G.add_edge("Grantaire", "Enjolras", weight=3)
+ G.add_edge("Grantaire", "Combeferre", weight=1)
+ G.add_edge("Grantaire", "Courfeyrac", weight=2)
+ G.add_edge("Grantaire", "Joly", weight=2)
+ G.add_edge("Grantaire", "Gavroche", weight=1)
+ G.add_edge("Grantaire", "Bahorel", weight=1)
+ G.add_edge("Grantaire", "Feuilly", weight=1)
+ G.add_edge("Grantaire", "Prouvaire", weight=1)
+ G.add_edge("MotherPlutarch", "Mabeuf", weight=3)
+ G.add_edge("Gueulemer", "Thenardier", weight=5)
+ G.add_edge("Gueulemer", "Valjean", weight=1)
+ G.add_edge("Gueulemer", "MmeThenardier", weight=1)
+ G.add_edge("Gueulemer", "Javert", weight=1)
+ G.add_edge("Gueulemer", "Gavroche", weight=1)
+ G.add_edge("Gueulemer", "Eponine", weight=1)
+ G.add_edge("Babet", "Thenardier", weight=6)
+ G.add_edge("Babet", "Gueulemer", weight=6)
+ G.add_edge("Babet", "Valjean", weight=1)
+ G.add_edge("Babet", "MmeThenardier", weight=1)
+ G.add_edge("Babet", "Javert", weight=2)
+ G.add_edge("Babet", "Gavroche", weight=1)
+ G.add_edge("Babet", "Eponine", weight=1)
+ G.add_edge("Claquesous", "Thenardier", weight=4)
+ G.add_edge("Claquesous", "Babet", weight=4)
+ G.add_edge("Claquesous", "Gueulemer", weight=4)
+ G.add_edge("Claquesous", "Valjean", weight=1)
+ G.add_edge("Claquesous", "MmeThenardier", weight=1)
+ G.add_edge("Claquesous", "Javert", weight=1)
+ G.add_edge("Claquesous", "Eponine", weight=1)
+ G.add_edge("Claquesous", "Enjolras", weight=1)
+ G.add_edge("Montparnasse", "Javert", weight=1)
+ G.add_edge("Montparnasse", "Babet", weight=2)
+ G.add_edge("Montparnasse", "Gueulemer", weight=2)
+ G.add_edge("Montparnasse", "Claquesous", weight=2)
+ G.add_edge("Montparnasse", "Valjean", weight=1)
+ G.add_edge("Montparnasse", "Gavroche", weight=1)
+ G.add_edge("Montparnasse", "Eponine", weight=1)
+ G.add_edge("Montparnasse", "Thenardier", weight=1)
+ G.add_edge("Toussaint", "Cosette", weight=2)
+ G.add_edge("Toussaint", "Javert", weight=1)
+ G.add_edge("Toussaint", "Valjean", weight=1)
+ G.add_edge("Child1", "Gavroche", weight=2)
+ G.add_edge("Child2", "Gavroche", weight=2)
+ G.add_edge("Child2", "Child1", weight=3)
+ G.add_edge("Brujon", "Babet", weight=3)
+ G.add_edge("Brujon", "Gueulemer", weight=3)
+ G.add_edge("Brujon", "Thenardier", weight=3)
+ G.add_edge("Brujon", "Gavroche", weight=1)
+ G.add_edge("Brujon", "Eponine", weight=1)
+ G.add_edge("Brujon", "Claquesous", weight=1)
+ G.add_edge("Brujon", "Montparnasse", weight=1)
+ G.add_edge("MmeHucheloup", "Bossuet", weight=1)
+ G.add_edge("MmeHucheloup", "Joly", weight=1)
+ G.add_edge("MmeHucheloup", "Grantaire", weight=1)
+ G.add_edge("MmeHucheloup", "Bahorel", weight=1)
+ G.add_edge("MmeHucheloup", "Courfeyrac", weight=1)
+ G.add_edge("MmeHucheloup", "Gavroche", weight=1)
+ G.add_edge("MmeHucheloup", "Enjolras", weight=1)
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/spectral_graph_forge.py b/.venv/lib/python3.12/site-packages/networkx/generators/spectral_graph_forge.py
new file mode 100644
index 00000000..39a87f74
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/spectral_graph_forge.py
@@ -0,0 +1,120 @@
+"""Generates graphs with a given eigenvector structure"""
+
+import networkx as nx
+from networkx.utils import np_random_state
+
+__all__ = ["spectral_graph_forge"]
+
+
+@np_random_state(3)
+@nx._dispatchable(returns_graph=True)
+def spectral_graph_forge(G, alpha, transformation="identity", seed=None):
+ """Returns a random simple graph with spectrum resembling that of `G`
+
+ This algorithm, called Spectral Graph Forge (SGF), computes the
+ eigenvectors of a given graph adjacency matrix, filters them and
+ builds a random graph with a similar eigenstructure.
+ SGF has been proved to be particularly useful for synthesizing
+ realistic social networks and it can also be used to anonymize
+ graph sensitive data.
+
+ Parameters
+ ----------
+ G : Graph
+ alpha : float
+ Ratio representing the percentage of eigenvectors of G to consider,
+ values in [0,1].
+ transformation : string, optional
+ Represents the intended matrix linear transformation, possible values
+ are 'identity' and 'modularity'
+ seed : integer, random_state, or None (default)
+ Indicator of numpy random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ H : Graph
+ A graph with a similar eigenvector structure of the input one.
+
+ Raises
+ ------
+ NetworkXError
+ If transformation has a value different from 'identity' or 'modularity'
+
+ Notes
+ -----
+ Spectral Graph Forge (SGF) generates a random simple graph resembling the
+ global properties of the given one.
+ It leverages the low-rank approximation of the associated adjacency matrix
+ driven by the *alpha* precision parameter.
+ SGF preserves the number of nodes of the input graph and their ordering.
+ This way, nodes of output graphs resemble the properties of the input one
+ and attributes can be directly mapped.
+
+ It considers the graph adjacency matrices which can optionally be
+ transformed to other symmetric real matrices (currently transformation
+ options include *identity* and *modularity*).
+ The *modularity* transformation, in the sense of Newman's modularity matrix
+ allows the focusing on community structure related properties of the graph.
+
+ SGF applies a low-rank approximation whose fixed rank is computed from the
+ ratio *alpha* of the input graph adjacency matrix dimension.
+ This step performs a filtering on the input eigenvectors similar to the low
+ pass filtering common in telecommunications.
+
+ The filtered values (after truncation) are used as input to a Bernoulli
+ sampling for constructing a random adjacency matrix.
+
+ References
+ ----------
+ .. [1] L. Baldesi, C. T. Butts, A. Markopoulou, "Spectral Graph Forge:
+ Graph Generation Targeting Modularity", IEEE Infocom, '18.
+ https://arxiv.org/abs/1801.01715
+ .. [2] M. Newman, "Networks: an introduction", Oxford university press,
+ 2010
+
+ Examples
+ --------
+ >>> G = nx.karate_club_graph()
+ >>> H = nx.spectral_graph_forge(G, 0.3)
+ >>>
+ """
+ import numpy as np
+ import scipy as sp
+
+ available_transformations = ["identity", "modularity"]
+ alpha = np.clip(alpha, 0, 1)
+ A = nx.to_numpy_array(G)
+ n = A.shape[1]
+ level = round(n * alpha)
+
+ if transformation not in available_transformations:
+ msg = f"{transformation!r} is not a valid transformation. "
+ msg += f"Transformations: {available_transformations}"
+ raise nx.NetworkXError(msg)
+
+ K = np.ones((1, n)) @ A
+
+ B = A
+ if transformation == "modularity":
+ B -= K.T @ K / K.sum()
+
+ # Compute low-rank approximation of B
+ evals, evecs = np.linalg.eigh(B)
+ k = np.argsort(np.abs(evals))[::-1] # indices of evals in descending order
+ evecs[:, k[np.arange(level, n)]] = 0 # set smallest eigenvectors to 0
+ B = evecs @ np.diag(evals) @ evecs.T
+
+ if transformation == "modularity":
+ B += K.T @ K / K.sum()
+
+ B = np.clip(B, 0, 1)
+ np.fill_diagonal(B, 0)
+
+ for i in range(n - 1):
+ B[i, i + 1 :] = sp.stats.bernoulli.rvs(B[i, i + 1 :], random_state=seed)
+ B[i + 1 :, i] = np.transpose(B[i, i + 1 :])
+
+ H = nx.from_numpy_array(B)
+
+ return H
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/stochastic.py b/.venv/lib/python3.12/site-packages/networkx/generators/stochastic.py
new file mode 100644
index 00000000..f53e2315
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/stochastic.py
@@ -0,0 +1,54 @@
+"""Functions for generating stochastic graphs from a given weighted directed
+graph.
+
+"""
+
+import networkx as nx
+from networkx.classes import DiGraph, MultiDiGraph
+from networkx.utils import not_implemented_for
+
+__all__ = ["stochastic_graph"]
+
+
+@not_implemented_for("undirected")
+@nx._dispatchable(
+ edge_attrs="weight", mutates_input={"not copy": 1}, returns_graph=True
+)
+def stochastic_graph(G, copy=True, weight="weight"):
+ """Returns a right-stochastic representation of directed graph `G`.
+
+ A right-stochastic graph is a weighted digraph in which for each
+ node, the sum of the weights of all the out-edges of that node is
+ 1. If the graph is already weighted (for example, via a 'weight'
+ edge attribute), the reweighting takes that into account.
+
+ Parameters
+ ----------
+ G : directed graph
+ A :class:`~networkx.DiGraph` or :class:`~networkx.MultiDiGraph`.
+
+ copy : boolean, optional
+ If this is True, then this function returns a new graph with
+ the stochastic reweighting. Otherwise, the original graph is
+ modified in-place (and also returned, for convenience).
+
+ weight : edge attribute key (optional, default='weight')
+ Edge attribute key used for reading the existing weight and
+ setting the new weight. If no attribute with this key is found
+ for an edge, then the edge weight is assumed to be 1. If an edge
+ has a weight, it must be a positive number.
+
+ """
+ if copy:
+ G = MultiDiGraph(G) if G.is_multigraph() else DiGraph(G)
+ # There is a tradeoff here: the dictionary of node degrees may
+ # require a lot of memory, whereas making a call to `G.out_degree`
+ # inside the loop may be costly in computation time.
+ degree = dict(G.out_degree(weight=weight))
+ for u, v, d in G.edges(data=True):
+ if degree[u] == 0:
+ d[weight] = 0
+ else:
+ d[weight] = d.get(weight, 1) / degree[u]
+ nx._clear_cache(G)
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/sudoku.py b/.venv/lib/python3.12/site-packages/networkx/generators/sudoku.py
new file mode 100644
index 00000000..f288ed24
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/sudoku.py
@@ -0,0 +1,131 @@
+"""Generator for Sudoku graphs
+
+This module gives a generator for n-Sudoku graphs. It can be used to develop
+algorithms for solving or generating Sudoku puzzles.
+
+A completed Sudoku grid is a 9x9 array of integers between 1 and 9, with no
+number appearing twice in the same row, column, or 3x3 box.
+
++---------+---------+---------+
+| | 8 6 4 | | 3 7 1 | | 2 5 9 |
+| | 3 2 5 | | 8 4 9 | | 7 6 1 |
+| | 9 7 1 | | 2 6 5 | | 8 4 3 |
++---------+---------+---------+
+| | 4 3 6 | | 1 9 2 | | 5 8 7 |
+| | 1 9 8 | | 6 5 7 | | 4 3 2 |
+| | 2 5 7 | | 4 8 3 | | 9 1 6 |
++---------+---------+---------+
+| | 6 8 9 | | 7 3 4 | | 1 2 5 |
+| | 7 1 3 | | 5 2 8 | | 6 9 4 |
+| | 5 4 2 | | 9 1 6 | | 3 7 8 |
++---------+---------+---------+
+
+
+The Sudoku graph is an undirected graph with 81 vertices, corresponding to
+the cells of a Sudoku grid. It is a regular graph of degree 20. Two distinct
+vertices are adjacent if and only if the corresponding cells belong to the
+same row, column, or box. A completed Sudoku grid corresponds to a vertex
+coloring of the Sudoku graph with nine colors.
+
+More generally, the n-Sudoku graph is a graph with n^4 vertices, corresponding
+to the cells of an n^2 by n^2 grid. Two distinct vertices are adjacent if and
+only if they belong to the same row, column, or n by n box.
+
+References
+----------
+.. [1] Herzberg, A. M., & Murty, M. R. (2007). Sudoku squares and chromatic
+ polynomials. Notices of the AMS, 54(6), 708-717.
+.. [2] Sander, Torsten (2009), "Sudoku graphs are integral",
+ Electronic Journal of Combinatorics, 16 (1): Note 25, 7pp, MR 2529816
+.. [3] Wikipedia contributors. "Glossary of Sudoku." Wikipedia, The Free
+ Encyclopedia, 3 Dec. 2019. Web. 22 Dec. 2019.
+"""
+
+import networkx as nx
+from networkx.exception import NetworkXError
+
+__all__ = ["sudoku_graph"]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def sudoku_graph(n=3):
+ """Returns the n-Sudoku graph. The default value of n is 3.
+
+ The n-Sudoku graph is a graph with n^4 vertices, corresponding to the
+ cells of an n^2 by n^2 grid. Two distinct vertices are adjacent if and
+ only if they belong to the same row, column, or n-by-n box.
+
+ Parameters
+ ----------
+ n: integer
+ The order of the Sudoku graph, equal to the square root of the
+ number of rows. The default is 3.
+
+ Returns
+ -------
+ NetworkX graph
+ The n-Sudoku graph Sud(n).
+
+ Examples
+ --------
+ >>> G = nx.sudoku_graph()
+ >>> G.number_of_nodes()
+ 81
+ >>> G.number_of_edges()
+ 810
+ >>> sorted(G.neighbors(42))
+ [6, 15, 24, 33, 34, 35, 36, 37, 38, 39, 40, 41, 43, 44, 51, 52, 53, 60, 69, 78]
+ >>> G = nx.sudoku_graph(2)
+ >>> G.number_of_nodes()
+ 16
+ >>> G.number_of_edges()
+ 56
+
+ References
+ ----------
+ .. [1] Herzberg, A. M., & Murty, M. R. (2007). Sudoku squares and chromatic
+ polynomials. Notices of the AMS, 54(6), 708-717.
+ .. [2] Sander, Torsten (2009), "Sudoku graphs are integral",
+ Electronic Journal of Combinatorics, 16 (1): Note 25, 7pp, MR 2529816
+ .. [3] Wikipedia contributors. "Glossary of Sudoku." Wikipedia, The Free
+ Encyclopedia, 3 Dec. 2019. Web. 22 Dec. 2019.
+ """
+
+ if n < 0:
+ raise NetworkXError("The order must be greater than or equal to zero.")
+
+ n2 = n * n
+ n3 = n2 * n
+ n4 = n3 * n
+
+ # Construct an empty graph with n^4 nodes
+ G = nx.empty_graph(n4)
+
+ # A Sudoku graph of order 0 or 1 has no edges
+ if n < 2:
+ return G
+
+ # Add edges for cells in the same row
+ for row_no in range(n2):
+ row_start = row_no * n2
+ for j in range(1, n2):
+ for i in range(j):
+ G.add_edge(row_start + i, row_start + j)
+
+ # Add edges for cells in the same column
+ for col_no in range(n2):
+ for j in range(col_no, n4, n2):
+ for i in range(col_no, j, n2):
+ G.add_edge(i, j)
+
+ # Add edges for cells in the same box
+ for band_no in range(n):
+ for stack_no in range(n):
+ box_start = n3 * band_no + n * stack_no
+ for j in range(1, n2):
+ for i in range(j):
+ u = box_start + (i % n) + n2 * (i // n)
+ v = box_start + (j % n) + n2 * (j // n)
+ G.add_edge(u, v)
+
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_atlas.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_atlas.py
new file mode 100644
index 00000000..add4741c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_atlas.py
@@ -0,0 +1,75 @@
+from itertools import groupby
+
+import pytest
+
+import networkx as nx
+from networkx import graph_atlas, graph_atlas_g
+from networkx.generators.atlas import NUM_GRAPHS
+from networkx.utils import edges_equal, nodes_equal, pairwise
+
+
+class TestAtlasGraph:
+ """Unit tests for the :func:`~networkx.graph_atlas` function."""
+
+ def test_index_too_small(self):
+ with pytest.raises(ValueError):
+ graph_atlas(-1)
+
+ def test_index_too_large(self):
+ with pytest.raises(ValueError):
+ graph_atlas(NUM_GRAPHS)
+
+ def test_graph(self):
+ G = graph_atlas(6)
+ assert nodes_equal(G.nodes(), range(3))
+ assert edges_equal(G.edges(), [(0, 1), (0, 2)])
+
+
+class TestAtlasGraphG:
+ """Unit tests for the :func:`~networkx.graph_atlas_g` function."""
+
+ @classmethod
+ def setup_class(cls):
+ cls.GAG = graph_atlas_g()
+
+ def test_sizes(self):
+ G = self.GAG[0]
+ assert G.number_of_nodes() == 0
+ assert G.number_of_edges() == 0
+
+ G = self.GAG[7]
+ assert G.number_of_nodes() == 3
+ assert G.number_of_edges() == 3
+
+ def test_names(self):
+ for i, G in enumerate(self.GAG):
+ assert int(G.name[1:]) == i
+
+ def test_nondecreasing_nodes(self):
+ # check for nondecreasing number of nodes
+ for n1, n2 in pairwise(map(len, self.GAG)):
+ assert n2 <= n1 + 1
+
+ def test_nondecreasing_edges(self):
+ # check for nondecreasing number of edges (for fixed number of
+ # nodes)
+ for n, group in groupby(self.GAG, key=nx.number_of_nodes):
+ for m1, m2 in pairwise(map(nx.number_of_edges, group)):
+ assert m2 <= m1 + 1
+
+ def test_nondecreasing_degree_sequence(self):
+ # Check for lexicographically nondecreasing degree sequences
+ # (for fixed number of nodes and edges).
+ #
+ # There are three exceptions to this rule in the order given in
+ # the "Atlas of Graphs" book, so we need to manually exclude
+ # those.
+ exceptions = [("G55", "G56"), ("G1007", "G1008"), ("G1012", "G1013")]
+ for n, group in groupby(self.GAG, key=nx.number_of_nodes):
+ for m, group in groupby(group, key=nx.number_of_edges):
+ for G1, G2 in pairwise(group):
+ if (G1.name, G2.name) in exceptions:
+ continue
+ d1 = sorted(d for v, d in G1.degree())
+ d2 = sorted(d for v, d in G2.degree())
+ assert d1 <= d2
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_classic.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_classic.py
new file mode 100644
index 00000000..9353c7f5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_classic.py
@@ -0,0 +1,640 @@
+"""
+====================
+Generators - Classic
+====================
+
+Unit tests for various classic graph generators in generators/classic.py
+"""
+
+import itertools
+import typing
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.isomorphism.isomorph import graph_could_be_isomorphic
+from networkx.utils import edges_equal, nodes_equal
+
+is_isomorphic = graph_could_be_isomorphic
+
+
+class TestGeneratorClassic:
+ def test_balanced_tree(self):
+ # balanced_tree(r,h) is a tree with (r**(h+1)-1)/(r-1) edges
+ for r, h in [(2, 2), (3, 3), (6, 2)]:
+ t = nx.balanced_tree(r, h)
+ order = t.order()
+ assert order == (r ** (h + 1) - 1) / (r - 1)
+ assert nx.is_connected(t)
+ assert t.size() == order - 1
+ dh = nx.degree_histogram(t)
+ assert dh[0] == 0 # no nodes of 0
+ assert dh[1] == r**h # nodes of degree 1 are leaves
+ assert dh[r] == 1 # root is degree r
+ assert dh[r + 1] == order - r**h - 1 # everyone else is degree r+1
+ assert len(dh) == r + 2
+
+ def test_balanced_tree_star(self):
+ # balanced_tree(r,1) is the r-star
+ t = nx.balanced_tree(r=2, h=1)
+ assert is_isomorphic(t, nx.star_graph(2))
+ t = nx.balanced_tree(r=5, h=1)
+ assert is_isomorphic(t, nx.star_graph(5))
+ t = nx.balanced_tree(r=10, h=1)
+ assert is_isomorphic(t, nx.star_graph(10))
+
+ def test_balanced_tree_path(self):
+ """Tests that the balanced tree with branching factor one is the
+ path graph.
+
+ """
+ # A tree of height four has five levels.
+ T = nx.balanced_tree(1, 4)
+ P = nx.path_graph(5)
+ assert is_isomorphic(T, P)
+
+ def test_full_rary_tree(self):
+ r = 2
+ n = 9
+ t = nx.full_rary_tree(r, n)
+ assert t.order() == n
+ assert nx.is_connected(t)
+ dh = nx.degree_histogram(t)
+ assert dh[0] == 0 # no nodes of 0
+ assert dh[1] == 5 # nodes of degree 1 are leaves
+ assert dh[r] == 1 # root is degree r
+ assert dh[r + 1] == 9 - 5 - 1 # everyone else is degree r+1
+ assert len(dh) == r + 2
+
+ def test_full_rary_tree_balanced(self):
+ t = nx.full_rary_tree(2, 15)
+ th = nx.balanced_tree(2, 3)
+ assert is_isomorphic(t, th)
+
+ def test_full_rary_tree_path(self):
+ t = nx.full_rary_tree(1, 10)
+ assert is_isomorphic(t, nx.path_graph(10))
+
+ def test_full_rary_tree_empty(self):
+ t = nx.full_rary_tree(0, 10)
+ assert is_isomorphic(t, nx.empty_graph(10))
+ t = nx.full_rary_tree(3, 0)
+ assert is_isomorphic(t, nx.empty_graph(0))
+
+ def test_full_rary_tree_3_20(self):
+ t = nx.full_rary_tree(3, 20)
+ assert t.order() == 20
+
+ def test_barbell_graph(self):
+ # number of nodes = 2*m1 + m2 (2 m1-complete graphs + m2-path + 2 edges)
+ # number of edges = 2*(nx.number_of_edges(m1-complete graph) + m2 + 1
+ m1 = 3
+ m2 = 5
+ b = nx.barbell_graph(m1, m2)
+ assert nx.number_of_nodes(b) == 2 * m1 + m2
+ assert nx.number_of_edges(b) == m1 * (m1 - 1) + m2 + 1
+
+ m1 = 4
+ m2 = 10
+ b = nx.barbell_graph(m1, m2)
+ assert nx.number_of_nodes(b) == 2 * m1 + m2
+ assert nx.number_of_edges(b) == m1 * (m1 - 1) + m2 + 1
+
+ m1 = 3
+ m2 = 20
+ b = nx.barbell_graph(m1, m2)
+ assert nx.number_of_nodes(b) == 2 * m1 + m2
+ assert nx.number_of_edges(b) == m1 * (m1 - 1) + m2 + 1
+
+ # Raise NetworkXError if m1<2
+ m1 = 1
+ m2 = 20
+ pytest.raises(nx.NetworkXError, nx.barbell_graph, m1, m2)
+
+ # Raise NetworkXError if m2<0
+ m1 = 5
+ m2 = -2
+ pytest.raises(nx.NetworkXError, nx.barbell_graph, m1, m2)
+
+ # nx.barbell_graph(2,m) = nx.path_graph(m+4)
+ m1 = 2
+ m2 = 5
+ b = nx.barbell_graph(m1, m2)
+ assert is_isomorphic(b, nx.path_graph(m2 + 4))
+
+ m1 = 2
+ m2 = 10
+ b = nx.barbell_graph(m1, m2)
+ assert is_isomorphic(b, nx.path_graph(m2 + 4))
+
+ m1 = 2
+ m2 = 20
+ b = nx.barbell_graph(m1, m2)
+ assert is_isomorphic(b, nx.path_graph(m2 + 4))
+
+ pytest.raises(
+ nx.NetworkXError, nx.barbell_graph, m1, m2, create_using=nx.DiGraph()
+ )
+
+ mb = nx.barbell_graph(m1, m2, create_using=nx.MultiGraph())
+ assert edges_equal(mb.edges(), b.edges())
+
+ def test_binomial_tree(self):
+ graphs = (None, nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
+ for create_using in graphs:
+ for n in range(4):
+ b = nx.binomial_tree(n, create_using)
+ assert nx.number_of_nodes(b) == 2**n
+ assert nx.number_of_edges(b) == (2**n - 1)
+
+ def test_complete_graph(self):
+ # complete_graph(m) is a connected graph with
+ # m nodes and m*(m+1)/2 edges
+ for m in [0, 1, 3, 5]:
+ g = nx.complete_graph(m)
+ assert nx.number_of_nodes(g) == m
+ assert nx.number_of_edges(g) == m * (m - 1) // 2
+
+ mg = nx.complete_graph(m, create_using=nx.MultiGraph)
+ assert edges_equal(mg.edges(), g.edges())
+
+ g = nx.complete_graph("abc")
+ assert nodes_equal(g.nodes(), ["a", "b", "c"])
+ assert g.size() == 3
+
+ # creates a self-loop... should it? <backward compatible says yes>
+ g = nx.complete_graph("abcb")
+ assert nodes_equal(g.nodes(), ["a", "b", "c"])
+ assert g.size() == 4
+
+ g = nx.complete_graph("abcb", create_using=nx.MultiGraph)
+ assert nodes_equal(g.nodes(), ["a", "b", "c"])
+ assert g.size() == 6
+
+ def test_complete_digraph(self):
+ # complete_graph(m) is a connected graph with
+ # m nodes and m*(m+1)/2 edges
+ for m in [0, 1, 3, 5]:
+ g = nx.complete_graph(m, create_using=nx.DiGraph)
+ assert nx.number_of_nodes(g) == m
+ assert nx.number_of_edges(g) == m * (m - 1)
+
+ g = nx.complete_graph("abc", create_using=nx.DiGraph)
+ assert len(g) == 3
+ assert g.size() == 6
+ assert g.is_directed()
+
+ def test_circular_ladder_graph(self):
+ G = nx.circular_ladder_graph(5)
+ pytest.raises(
+ nx.NetworkXError, nx.circular_ladder_graph, 5, create_using=nx.DiGraph
+ )
+ mG = nx.circular_ladder_graph(5, create_using=nx.MultiGraph)
+ assert edges_equal(mG.edges(), G.edges())
+
+ def test_circulant_graph(self):
+ # Ci_n(1) is the cycle graph for all n
+ Ci6_1 = nx.circulant_graph(6, [1])
+ C6 = nx.cycle_graph(6)
+ assert edges_equal(Ci6_1.edges(), C6.edges())
+
+ # Ci_n(1, 2, ..., n div 2) is the complete graph for all n
+ Ci7 = nx.circulant_graph(7, [1, 2, 3])
+ K7 = nx.complete_graph(7)
+ assert edges_equal(Ci7.edges(), K7.edges())
+
+ # Ci_6(1, 3) is K_3,3 i.e. the utility graph
+ Ci6_1_3 = nx.circulant_graph(6, [1, 3])
+ K3_3 = nx.complete_bipartite_graph(3, 3)
+ assert is_isomorphic(Ci6_1_3, K3_3)
+
+ def test_cycle_graph(self):
+ G = nx.cycle_graph(4)
+ assert edges_equal(G.edges(), [(0, 1), (0, 3), (1, 2), (2, 3)])
+ mG = nx.cycle_graph(4, create_using=nx.MultiGraph)
+ assert edges_equal(mG.edges(), [(0, 1), (0, 3), (1, 2), (2, 3)])
+ G = nx.cycle_graph(4, create_using=nx.DiGraph)
+ assert not G.has_edge(2, 1)
+ assert G.has_edge(1, 2)
+ assert G.is_directed()
+
+ G = nx.cycle_graph("abc")
+ assert len(G) == 3
+ assert G.size() == 3
+ G = nx.cycle_graph("abcb")
+ assert len(G) == 3
+ assert G.size() == 2
+ g = nx.cycle_graph("abc", nx.DiGraph)
+ assert len(g) == 3
+ assert g.size() == 3
+ assert g.is_directed()
+ g = nx.cycle_graph("abcb", nx.DiGraph)
+ assert len(g) == 3
+ assert g.size() == 4
+
+ def test_dorogovtsev_goltsev_mendes_graph(self):
+ G = nx.dorogovtsev_goltsev_mendes_graph(0)
+ assert edges_equal(G.edges(), [(0, 1)])
+ assert nodes_equal(list(G), [0, 1])
+ G = nx.dorogovtsev_goltsev_mendes_graph(1)
+ assert edges_equal(G.edges(), [(0, 1), (0, 2), (1, 2)])
+ assert nx.average_clustering(G) == 1.0
+ assert nx.average_shortest_path_length(G) == 1.0
+ assert sorted(nx.triangles(G).values()) == [1, 1, 1]
+ assert nx.is_planar(G)
+ G = nx.dorogovtsev_goltsev_mendes_graph(2)
+ assert nx.number_of_nodes(G) == 6
+ assert nx.number_of_edges(G) == 9
+ assert nx.average_clustering(G) == 0.75
+ assert nx.average_shortest_path_length(G) == 1.4
+ assert nx.is_planar(G)
+ G = nx.dorogovtsev_goltsev_mendes_graph(10)
+ assert nx.number_of_nodes(G) == 29526
+ assert nx.number_of_edges(G) == 59049
+ assert G.degree(0) == 1024
+ assert G.degree(1) == 1024
+ assert G.degree(2) == 1024
+
+ with pytest.raises(nx.NetworkXError, match=r"n must be greater than"):
+ nx.dorogovtsev_goltsev_mendes_graph(-1)
+ with pytest.raises(nx.NetworkXError, match=r"directed graph not supported"):
+ nx.dorogovtsev_goltsev_mendes_graph(7, create_using=nx.DiGraph)
+ with pytest.raises(nx.NetworkXError, match=r"multigraph not supported"):
+ nx.dorogovtsev_goltsev_mendes_graph(7, create_using=nx.MultiGraph)
+ with pytest.raises(nx.NetworkXError):
+ nx.dorogovtsev_goltsev_mendes_graph(7, create_using=nx.MultiDiGraph)
+
+ def test_create_using(self):
+ G = nx.empty_graph()
+ assert isinstance(G, nx.Graph)
+ pytest.raises(TypeError, nx.empty_graph, create_using=0.0)
+ pytest.raises(TypeError, nx.empty_graph, create_using="Graph")
+
+ G = nx.empty_graph(create_using=nx.MultiGraph)
+ assert isinstance(G, nx.MultiGraph)
+ G = nx.empty_graph(create_using=nx.DiGraph)
+ assert isinstance(G, nx.DiGraph)
+
+ G = nx.empty_graph(create_using=nx.DiGraph, default=nx.MultiGraph)
+ assert isinstance(G, nx.DiGraph)
+ G = nx.empty_graph(create_using=None, default=nx.MultiGraph)
+ assert isinstance(G, nx.MultiGraph)
+ G = nx.empty_graph(default=nx.MultiGraph)
+ assert isinstance(G, nx.MultiGraph)
+
+ G = nx.path_graph(5)
+ H = nx.empty_graph(create_using=G)
+ assert not H.is_multigraph()
+ assert not H.is_directed()
+ assert len(H) == 0
+ assert G is H
+
+ H = nx.empty_graph(create_using=nx.MultiGraph())
+ assert H.is_multigraph()
+ assert not H.is_directed()
+ assert G is not H
+
+ # test for subclasses that also use typing.Protocol. See gh-6243
+ class Mixin(typing.Protocol):
+ pass
+
+ class MyGraph(Mixin, nx.DiGraph):
+ pass
+
+ G = nx.empty_graph(create_using=MyGraph)
+
+ def test_empty_graph(self):
+ G = nx.empty_graph()
+ assert nx.number_of_nodes(G) == 0
+ G = nx.empty_graph(42)
+ assert nx.number_of_nodes(G) == 42
+ assert nx.number_of_edges(G) == 0
+
+ G = nx.empty_graph("abc")
+ assert len(G) == 3
+ assert G.size() == 0
+
+ # create empty digraph
+ G = nx.empty_graph(42, create_using=nx.DiGraph(name="duh"))
+ assert nx.number_of_nodes(G) == 42
+ assert nx.number_of_edges(G) == 0
+ assert isinstance(G, nx.DiGraph)
+
+ # create empty multigraph
+ G = nx.empty_graph(42, create_using=nx.MultiGraph(name="duh"))
+ assert nx.number_of_nodes(G) == 42
+ assert nx.number_of_edges(G) == 0
+ assert isinstance(G, nx.MultiGraph)
+
+ # create empty graph from another
+ pete = nx.petersen_graph()
+ G = nx.empty_graph(42, create_using=pete)
+ assert nx.number_of_nodes(G) == 42
+ assert nx.number_of_edges(G) == 0
+ assert isinstance(G, nx.Graph)
+
+ def test_ladder_graph(self):
+ for i, G in [
+ (0, nx.empty_graph(0)),
+ (1, nx.path_graph(2)),
+ (2, nx.hypercube_graph(2)),
+ (10, nx.grid_graph([2, 10])),
+ ]:
+ assert is_isomorphic(nx.ladder_graph(i), G)
+
+ pytest.raises(nx.NetworkXError, nx.ladder_graph, 2, create_using=nx.DiGraph)
+
+ g = nx.ladder_graph(2)
+ mg = nx.ladder_graph(2, create_using=nx.MultiGraph)
+ assert edges_equal(mg.edges(), g.edges())
+
+ @pytest.mark.parametrize(("m", "n"), [(3, 5), (4, 10), (3, 20)])
+ def test_lollipop_graph_right_sizes(self, m, n):
+ G = nx.lollipop_graph(m, n)
+ assert nx.number_of_nodes(G) == m + n
+ assert nx.number_of_edges(G) == m * (m - 1) / 2 + n
+
+ @pytest.mark.parametrize(("m", "n"), [("ab", ""), ("abc", "defg")])
+ def test_lollipop_graph_size_node_sequence(self, m, n):
+ G = nx.lollipop_graph(m, n)
+ assert nx.number_of_nodes(G) == len(m) + len(n)
+ assert nx.number_of_edges(G) == len(m) * (len(m) - 1) / 2 + len(n)
+
+ def test_lollipop_graph_exceptions(self):
+ # Raise NetworkXError if m<2
+ pytest.raises(nx.NetworkXError, nx.lollipop_graph, -1, 2)
+ pytest.raises(nx.NetworkXError, nx.lollipop_graph, 1, 20)
+ pytest.raises(nx.NetworkXError, nx.lollipop_graph, "", 20)
+ pytest.raises(nx.NetworkXError, nx.lollipop_graph, "a", 20)
+
+ # Raise NetworkXError if n<0
+ pytest.raises(nx.NetworkXError, nx.lollipop_graph, 5, -2)
+
+ # raise NetworkXError if create_using is directed
+ with pytest.raises(nx.NetworkXError):
+ nx.lollipop_graph(2, 20, create_using=nx.DiGraph)
+ with pytest.raises(nx.NetworkXError):
+ nx.lollipop_graph(2, 20, create_using=nx.MultiDiGraph)
+
+ @pytest.mark.parametrize(("m", "n"), [(2, 0), (2, 5), (2, 10), ("ab", 20)])
+ def test_lollipop_graph_same_as_path_when_m1_is_2(self, m, n):
+ G = nx.lollipop_graph(m, n)
+ assert is_isomorphic(G, nx.path_graph(n + 2))
+
+ def test_lollipop_graph_for_multigraph(self):
+ G = nx.lollipop_graph(5, 20)
+ MG = nx.lollipop_graph(5, 20, create_using=nx.MultiGraph)
+ assert edges_equal(MG.edges(), G.edges())
+
+ @pytest.mark.parametrize(
+ ("m", "n"),
+ [(4, "abc"), ("abcd", 3), ([1, 2, 3, 4], "abc"), ("abcd", [1, 2, 3])],
+ )
+ def test_lollipop_graph_mixing_input_types(self, m, n):
+ expected = nx.compose(nx.complete_graph(4), nx.path_graph(range(100, 103)))
+ expected.add_edge(0, 100) # Connect complete graph and path graph
+ assert is_isomorphic(nx.lollipop_graph(m, n), expected)
+
+ def test_lollipop_graph_non_builtin_ints(self):
+ np = pytest.importorskip("numpy")
+ G = nx.lollipop_graph(np.int32(4), np.int64(3))
+ expected = nx.compose(nx.complete_graph(4), nx.path_graph(range(100, 103)))
+ expected.add_edge(0, 100) # Connect complete graph and path graph
+ assert is_isomorphic(G, expected)
+
+ def test_null_graph(self):
+ assert nx.number_of_nodes(nx.null_graph()) == 0
+
+ def test_path_graph(self):
+ p = nx.path_graph(0)
+ assert is_isomorphic(p, nx.null_graph())
+
+ p = nx.path_graph(1)
+ assert is_isomorphic(p, nx.empty_graph(1))
+
+ p = nx.path_graph(10)
+ assert nx.is_connected(p)
+ assert sorted(d for n, d in p.degree()) == [1, 1, 2, 2, 2, 2, 2, 2, 2, 2]
+ assert p.order() - 1 == p.size()
+
+ dp = nx.path_graph(3, create_using=nx.DiGraph)
+ assert dp.has_edge(0, 1)
+ assert not dp.has_edge(1, 0)
+
+ mp = nx.path_graph(10, create_using=nx.MultiGraph)
+ assert edges_equal(mp.edges(), p.edges())
+
+ G = nx.path_graph("abc")
+ assert len(G) == 3
+ assert G.size() == 2
+ G = nx.path_graph("abcb")
+ assert len(G) == 3
+ assert G.size() == 2
+ g = nx.path_graph("abc", nx.DiGraph)
+ assert len(g) == 3
+ assert g.size() == 2
+ assert g.is_directed()
+ g = nx.path_graph("abcb", nx.DiGraph)
+ assert len(g) == 3
+ assert g.size() == 3
+
+ G = nx.path_graph((1, 2, 3, 2, 4))
+ assert G.has_edge(2, 4)
+
+ def test_star_graph(self):
+ assert is_isomorphic(nx.star_graph(""), nx.empty_graph(0))
+ assert is_isomorphic(nx.star_graph([]), nx.empty_graph(0))
+ assert is_isomorphic(nx.star_graph(0), nx.empty_graph(1))
+ assert is_isomorphic(nx.star_graph(1), nx.path_graph(2))
+ assert is_isomorphic(nx.star_graph(2), nx.path_graph(3))
+ assert is_isomorphic(nx.star_graph(5), nx.complete_bipartite_graph(1, 5))
+
+ s = nx.star_graph(10)
+ assert sorted(d for n, d in s.degree()) == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 10]
+
+ pytest.raises(nx.NetworkXError, nx.star_graph, 10, create_using=nx.DiGraph)
+
+ ms = nx.star_graph(10, create_using=nx.MultiGraph)
+ assert edges_equal(ms.edges(), s.edges())
+
+ G = nx.star_graph("abc")
+ assert len(G) == 3
+ assert G.size() == 2
+
+ G = nx.star_graph("abcb")
+ assert len(G) == 3
+ assert G.size() == 2
+ G = nx.star_graph("abcb", create_using=nx.MultiGraph)
+ assert len(G) == 3
+ assert G.size() == 3
+
+ G = nx.star_graph("abcdefg")
+ assert len(G) == 7
+ assert G.size() == 6
+
+ def test_non_int_integers_for_star_graph(self):
+ np = pytest.importorskip("numpy")
+ G = nx.star_graph(np.int32(3))
+ assert len(G) == 4
+ assert G.size() == 3
+
+ @pytest.mark.parametrize(("m", "n"), [(3, 0), (3, 5), (4, 10), (3, 20)])
+ def test_tadpole_graph_right_sizes(self, m, n):
+ G = nx.tadpole_graph(m, n)
+ assert nx.number_of_nodes(G) == m + n
+ assert nx.number_of_edges(G) == m + n - (m == 2)
+
+ @pytest.mark.parametrize(("m", "n"), [("ab", ""), ("ab", "c"), ("abc", "defg")])
+ def test_tadpole_graph_size_node_sequences(self, m, n):
+ G = nx.tadpole_graph(m, n)
+ assert nx.number_of_nodes(G) == len(m) + len(n)
+ assert nx.number_of_edges(G) == len(m) + len(n) - (len(m) == 2)
+
+ def test_tadpole_graph_exceptions(self):
+ # Raise NetworkXError if m<2
+ pytest.raises(nx.NetworkXError, nx.tadpole_graph, -1, 3)
+ pytest.raises(nx.NetworkXError, nx.tadpole_graph, 0, 3)
+ pytest.raises(nx.NetworkXError, nx.tadpole_graph, 1, 3)
+
+ # Raise NetworkXError if n<0
+ pytest.raises(nx.NetworkXError, nx.tadpole_graph, 5, -2)
+
+ # Raise NetworkXError for digraphs
+ with pytest.raises(nx.NetworkXError):
+ nx.tadpole_graph(2, 20, create_using=nx.DiGraph)
+ with pytest.raises(nx.NetworkXError):
+ nx.tadpole_graph(2, 20, create_using=nx.MultiDiGraph)
+
+ @pytest.mark.parametrize(("m", "n"), [(2, 0), (2, 5), (2, 10), ("ab", 20)])
+ def test_tadpole_graph_same_as_path_when_m_is_2(self, m, n):
+ G = nx.tadpole_graph(m, n)
+ assert is_isomorphic(G, nx.path_graph(n + 2))
+
+ @pytest.mark.parametrize("m", [4, 7])
+ def test_tadpole_graph_same_as_cycle_when_m2_is_0(self, m):
+ G = nx.tadpole_graph(m, 0)
+ assert is_isomorphic(G, nx.cycle_graph(m))
+
+ def test_tadpole_graph_for_multigraph(self):
+ G = nx.tadpole_graph(5, 20)
+ MG = nx.tadpole_graph(5, 20, create_using=nx.MultiGraph)
+ assert edges_equal(MG.edges(), G.edges())
+
+ @pytest.mark.parametrize(
+ ("m", "n"),
+ [(4, "abc"), ("abcd", 3), ([1, 2, 3, 4], "abc"), ("abcd", [1, 2, 3])],
+ )
+ def test_tadpole_graph_mixing_input_types(self, m, n):
+ expected = nx.compose(nx.cycle_graph(4), nx.path_graph(range(100, 103)))
+ expected.add_edge(0, 100) # Connect cycle and path
+ assert is_isomorphic(nx.tadpole_graph(m, n), expected)
+
+ def test_tadpole_graph_non_builtin_integers(self):
+ np = pytest.importorskip("numpy")
+ G = nx.tadpole_graph(np.int32(4), np.int64(3))
+ expected = nx.compose(nx.cycle_graph(4), nx.path_graph(range(100, 103)))
+ expected.add_edge(0, 100) # Connect cycle and path
+ assert is_isomorphic(G, expected)
+
+ def test_trivial_graph(self):
+ assert nx.number_of_nodes(nx.trivial_graph()) == 1
+
+ def test_turan_graph(self):
+ assert nx.number_of_edges(nx.turan_graph(13, 4)) == 63
+ assert is_isomorphic(
+ nx.turan_graph(13, 4), nx.complete_multipartite_graph(3, 4, 3, 3)
+ )
+
+ def test_wheel_graph(self):
+ for n, G in [
+ ("", nx.null_graph()),
+ (0, nx.null_graph()),
+ (1, nx.empty_graph(1)),
+ (2, nx.path_graph(2)),
+ (3, nx.complete_graph(3)),
+ (4, nx.complete_graph(4)),
+ ]:
+ g = nx.wheel_graph(n)
+ assert is_isomorphic(g, G)
+
+ g = nx.wheel_graph(10)
+ assert sorted(d for n, d in g.degree()) == [3, 3, 3, 3, 3, 3, 3, 3, 3, 9]
+
+ pytest.raises(nx.NetworkXError, nx.wheel_graph, 10, create_using=nx.DiGraph)
+
+ mg = nx.wheel_graph(10, create_using=nx.MultiGraph())
+ assert edges_equal(mg.edges(), g.edges())
+
+ G = nx.wheel_graph("abc")
+ assert len(G) == 3
+ assert G.size() == 3
+
+ G = nx.wheel_graph("abcb")
+ assert len(G) == 3
+ assert G.size() == 4
+ G = nx.wheel_graph("abcb", nx.MultiGraph)
+ assert len(G) == 3
+ assert G.size() == 6
+
+ def test_non_int_integers_for_wheel_graph(self):
+ np = pytest.importorskip("numpy")
+ G = nx.wheel_graph(np.int32(3))
+ assert len(G) == 3
+ assert G.size() == 3
+
+ def test_complete_0_partite_graph(self):
+ """Tests that the complete 0-partite graph is the null graph."""
+ G = nx.complete_multipartite_graph()
+ H = nx.null_graph()
+ assert nodes_equal(G, H)
+ assert edges_equal(G.edges(), H.edges())
+
+ def test_complete_1_partite_graph(self):
+ """Tests that the complete 1-partite graph is the empty graph."""
+ G = nx.complete_multipartite_graph(3)
+ H = nx.empty_graph(3)
+ assert nodes_equal(G, H)
+ assert edges_equal(G.edges(), H.edges())
+
+ def test_complete_2_partite_graph(self):
+ """Tests that the complete 2-partite graph is the complete bipartite
+ graph.
+
+ """
+ G = nx.complete_multipartite_graph(2, 3)
+ H = nx.complete_bipartite_graph(2, 3)
+ assert nodes_equal(G, H)
+ assert edges_equal(G.edges(), H.edges())
+
+ def test_complete_multipartite_graph(self):
+ """Tests for generating the complete multipartite graph."""
+ G = nx.complete_multipartite_graph(2, 3, 4)
+ blocks = [(0, 1), (2, 3, 4), (5, 6, 7, 8)]
+ # Within each block, no two vertices should be adjacent.
+ for block in blocks:
+ for u, v in itertools.combinations_with_replacement(block, 2):
+ assert v not in G[u]
+ assert G.nodes[u] == G.nodes[v]
+ # Across blocks, all vertices should be adjacent.
+ for block1, block2 in itertools.combinations(blocks, 2):
+ for u, v in itertools.product(block1, block2):
+ assert v in G[u]
+ assert G.nodes[u] != G.nodes[v]
+ with pytest.raises(nx.NetworkXError, match="Negative number of nodes"):
+ nx.complete_multipartite_graph(2, -3, 4)
+
+ def test_kneser_graph(self):
+ # the petersen graph is a special case of the kneser graph when n=5 and k=2
+ assert is_isomorphic(nx.kneser_graph(5, 2), nx.petersen_graph())
+
+ # when k is 1, the kneser graph returns a complete graph with n vertices
+ for i in range(1, 7):
+ assert is_isomorphic(nx.kneser_graph(i, 1), nx.complete_graph(i))
+
+ # the kneser graph of n and n-1 is the empty graph with n vertices
+ for j in range(3, 7):
+ assert is_isomorphic(nx.kneser_graph(j, j - 1), nx.empty_graph(j))
+
+ # in general the number of edges of the kneser graph is equal to
+ # (n choose k) times (n-k choose k) divided by 2
+ assert nx.number_of_edges(nx.kneser_graph(8, 3)) == 280
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_cographs.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_cographs.py
new file mode 100644
index 00000000..a71849b0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_cographs.py
@@ -0,0 +1,18 @@
+"""Unit tests for the :mod:`networkx.generators.cographs` module."""
+
+import networkx as nx
+
+
+def test_random_cograph():
+ n = 3
+ G = nx.random_cograph(n)
+
+ assert len(G) == 2**n
+
+ # Every connected subgraph of G has diameter <= 2
+ if nx.is_connected(G):
+ assert nx.diameter(G) <= 2
+ else:
+ components = nx.connected_components(G)
+ for component in components:
+ assert nx.diameter(G.subgraph(component)) <= 2
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_community.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_community.py
new file mode 100644
index 00000000..2fa107f6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_community.py
@@ -0,0 +1,362 @@
+import pytest
+
+import networkx as nx
+
+
+def test_random_partition_graph():
+ G = nx.random_partition_graph([3, 3, 3], 1, 0, seed=42)
+ C = G.graph["partition"]
+ assert C == [{0, 1, 2}, {3, 4, 5}, {6, 7, 8}]
+ assert len(G) == 9
+ assert len(list(G.edges())) == 9
+
+ G = nx.random_partition_graph([3, 3, 3], 0, 1)
+ C = G.graph["partition"]
+ assert C == [{0, 1, 2}, {3, 4, 5}, {6, 7, 8}]
+ assert len(G) == 9
+ assert len(list(G.edges())) == 27
+
+ G = nx.random_partition_graph([3, 3, 3], 1, 0, directed=True)
+ C = G.graph["partition"]
+ assert C == [{0, 1, 2}, {3, 4, 5}, {6, 7, 8}]
+ assert len(G) == 9
+ assert len(list(G.edges())) == 18
+
+ G = nx.random_partition_graph([3, 3, 3], 0, 1, directed=True)
+ C = G.graph["partition"]
+ assert C == [{0, 1, 2}, {3, 4, 5}, {6, 7, 8}]
+ assert len(G) == 9
+ assert len(list(G.edges())) == 54
+
+ G = nx.random_partition_graph([1, 2, 3, 4, 5], 0.5, 0.1)
+ C = G.graph["partition"]
+ assert C == [{0}, {1, 2}, {3, 4, 5}, {6, 7, 8, 9}, {10, 11, 12, 13, 14}]
+ assert len(G) == 15
+
+ rpg = nx.random_partition_graph
+ pytest.raises(nx.NetworkXError, rpg, [1, 2, 3], 1.1, 0.1)
+ pytest.raises(nx.NetworkXError, rpg, [1, 2, 3], -0.1, 0.1)
+ pytest.raises(nx.NetworkXError, rpg, [1, 2, 3], 0.1, 1.1)
+ pytest.raises(nx.NetworkXError, rpg, [1, 2, 3], 0.1, -0.1)
+
+
+def test_planted_partition_graph():
+ G = nx.planted_partition_graph(4, 3, 1, 0, seed=42)
+ C = G.graph["partition"]
+ assert len(C) == 4
+ assert len(G) == 12
+ assert len(list(G.edges())) == 12
+
+ G = nx.planted_partition_graph(4, 3, 0, 1)
+ C = G.graph["partition"]
+ assert len(C) == 4
+ assert len(G) == 12
+ assert len(list(G.edges())) == 54
+
+ G = nx.planted_partition_graph(10, 4, 0.5, 0.1, seed=42)
+ C = G.graph["partition"]
+ assert len(C) == 10
+ assert len(G) == 40
+
+ G = nx.planted_partition_graph(4, 3, 1, 0, directed=True)
+ C = G.graph["partition"]
+ assert len(C) == 4
+ assert len(G) == 12
+ assert len(list(G.edges())) == 24
+
+ G = nx.planted_partition_graph(4, 3, 0, 1, directed=True)
+ C = G.graph["partition"]
+ assert len(C) == 4
+ assert len(G) == 12
+ assert len(list(G.edges())) == 108
+
+ G = nx.planted_partition_graph(10, 4, 0.5, 0.1, seed=42, directed=True)
+ C = G.graph["partition"]
+ assert len(C) == 10
+ assert len(G) == 40
+
+ ppg = nx.planted_partition_graph
+ pytest.raises(nx.NetworkXError, ppg, 3, 3, 1.1, 0.1)
+ pytest.raises(nx.NetworkXError, ppg, 3, 3, -0.1, 0.1)
+ pytest.raises(nx.NetworkXError, ppg, 3, 3, 0.1, 1.1)
+ pytest.raises(nx.NetworkXError, ppg, 3, 3, 0.1, -0.1)
+
+
+def test_relaxed_caveman_graph():
+ G = nx.relaxed_caveman_graph(4, 3, 0)
+ assert len(G) == 12
+ G = nx.relaxed_caveman_graph(4, 3, 1)
+ assert len(G) == 12
+ G = nx.relaxed_caveman_graph(4, 3, 0.5)
+ assert len(G) == 12
+ G = nx.relaxed_caveman_graph(4, 3, 0.5, seed=42)
+ assert len(G) == 12
+
+
+def test_connected_caveman_graph():
+ G = nx.connected_caveman_graph(4, 3)
+ assert len(G) == 12
+
+ G = nx.connected_caveman_graph(1, 5)
+ K5 = nx.complete_graph(5)
+ K5.remove_edge(3, 4)
+ assert nx.is_isomorphic(G, K5)
+
+ # need at least 2 nodes in each clique
+ pytest.raises(nx.NetworkXError, nx.connected_caveman_graph, 4, 1)
+
+
+def test_caveman_graph():
+ G = nx.caveman_graph(4, 3)
+ assert len(G) == 12
+
+ G = nx.caveman_graph(5, 1)
+ E5 = nx.empty_graph(5)
+ assert nx.is_isomorphic(G, E5)
+
+ G = nx.caveman_graph(1, 5)
+ K5 = nx.complete_graph(5)
+ assert nx.is_isomorphic(G, K5)
+
+
+def test_gaussian_random_partition_graph():
+ G = nx.gaussian_random_partition_graph(100, 10, 10, 0.3, 0.01)
+ assert len(G) == 100
+ G = nx.gaussian_random_partition_graph(100, 10, 10, 0.3, 0.01, directed=True)
+ assert len(G) == 100
+ G = nx.gaussian_random_partition_graph(
+ 100, 10, 10, 0.3, 0.01, directed=False, seed=42
+ )
+ assert len(G) == 100
+ assert not isinstance(G, nx.DiGraph)
+ G = nx.gaussian_random_partition_graph(
+ 100, 10, 10, 0.3, 0.01, directed=True, seed=42
+ )
+ assert len(G) == 100
+ assert isinstance(G, nx.DiGraph)
+ pytest.raises(
+ nx.NetworkXError, nx.gaussian_random_partition_graph, 100, 101, 10, 1, 0
+ )
+ # Test when clusters are likely less than 1
+ G = nx.gaussian_random_partition_graph(10, 0.5, 0.5, 0.5, 0.5, seed=1)
+ assert len(G) == 10
+
+
+def test_ring_of_cliques():
+ for i in range(2, 20, 3):
+ for j in range(2, 20, 3):
+ G = nx.ring_of_cliques(i, j)
+ assert G.number_of_nodes() == i * j
+ if i != 2 or j != 1:
+ expected_num_edges = i * (((j * (j - 1)) // 2) + 1)
+ else:
+ # the edge that already exists cannot be duplicated
+ expected_num_edges = i * (((j * (j - 1)) // 2) + 1) - 1
+ assert G.number_of_edges() == expected_num_edges
+ with pytest.raises(
+ nx.NetworkXError, match="A ring of cliques must have at least two cliques"
+ ):
+ nx.ring_of_cliques(1, 5)
+ with pytest.raises(
+ nx.NetworkXError, match="The cliques must have at least two nodes"
+ ):
+ nx.ring_of_cliques(3, 0)
+
+
+def test_windmill_graph():
+ for n in range(2, 20, 3):
+ for k in range(2, 20, 3):
+ G = nx.windmill_graph(n, k)
+ assert G.number_of_nodes() == (k - 1) * n + 1
+ assert G.number_of_edges() == n * k * (k - 1) / 2
+ assert G.degree(0) == G.number_of_nodes() - 1
+ for i in range(1, G.number_of_nodes()):
+ assert G.degree(i) == k - 1
+ with pytest.raises(
+ nx.NetworkXError, match="A windmill graph must have at least two cliques"
+ ):
+ nx.windmill_graph(1, 3)
+ with pytest.raises(
+ nx.NetworkXError, match="The cliques must have at least two nodes"
+ ):
+ nx.windmill_graph(3, 0)
+
+
+def test_stochastic_block_model():
+ sizes = [75, 75, 300]
+ probs = [[0.25, 0.05, 0.02], [0.05, 0.35, 0.07], [0.02, 0.07, 0.40]]
+ G = nx.stochastic_block_model(sizes, probs, seed=0)
+ C = G.graph["partition"]
+ assert len(C) == 3
+ assert len(G) == 450
+ assert G.size() == 22160
+
+ GG = nx.stochastic_block_model(sizes, probs, range(450), seed=0)
+ assert G.nodes == GG.nodes
+
+ # Test Exceptions
+ sbm = nx.stochastic_block_model
+ badnodelist = list(range(400)) # not enough nodes to match sizes
+ badprobs1 = [[0.25, 0.05, 1.02], [0.05, 0.35, 0.07], [0.02, 0.07, 0.40]]
+ badprobs2 = [[0.25, 0.05, 0.02], [0.05, -0.35, 0.07], [0.02, 0.07, 0.40]]
+ probs_rect1 = [[0.25, 0.05, 0.02], [0.05, -0.35, 0.07]]
+ probs_rect2 = [[0.25, 0.05], [0.05, -0.35], [0.02, 0.07]]
+ asymprobs = [[0.25, 0.05, 0.01], [0.05, -0.35, 0.07], [0.02, 0.07, 0.40]]
+ pytest.raises(nx.NetworkXException, sbm, sizes, badprobs1)
+ pytest.raises(nx.NetworkXException, sbm, sizes, badprobs2)
+ pytest.raises(nx.NetworkXException, sbm, sizes, probs_rect1, directed=True)
+ pytest.raises(nx.NetworkXException, sbm, sizes, probs_rect2, directed=True)
+ pytest.raises(nx.NetworkXException, sbm, sizes, asymprobs, directed=False)
+ pytest.raises(nx.NetworkXException, sbm, sizes, probs, badnodelist)
+ nodelist = [0] + list(range(449)) # repeated node name in nodelist
+ pytest.raises(nx.NetworkXException, sbm, sizes, probs, nodelist)
+
+ # Extra keyword arguments test
+ GG = nx.stochastic_block_model(sizes, probs, seed=0, selfloops=True)
+ assert G.nodes == GG.nodes
+ GG = nx.stochastic_block_model(sizes, probs, selfloops=True, directed=True)
+ assert G.nodes == GG.nodes
+ GG = nx.stochastic_block_model(sizes, probs, seed=0, sparse=False)
+ assert G.nodes == GG.nodes
+
+
+def test_generator():
+ n = 250
+ tau1 = 3
+ tau2 = 1.5
+ mu = 0.1
+ G = nx.LFR_benchmark_graph(
+ n, tau1, tau2, mu, average_degree=5, min_community=20, seed=10
+ )
+ assert len(G) == 250
+ C = {frozenset(G.nodes[v]["community"]) for v in G}
+ assert nx.community.is_partition(G.nodes(), C)
+
+
+def test_invalid_tau1():
+ with pytest.raises(nx.NetworkXError, match="tau2 must be greater than one"):
+ n = 100
+ tau1 = 2
+ tau2 = 1
+ mu = 0.1
+ nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2)
+
+
+def test_invalid_tau2():
+ with pytest.raises(nx.NetworkXError, match="tau1 must be greater than one"):
+ n = 100
+ tau1 = 1
+ tau2 = 2
+ mu = 0.1
+ nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2)
+
+
+def test_mu_too_large():
+ with pytest.raises(nx.NetworkXError, match="mu must be in the interval \\[0, 1\\]"):
+ n = 100
+ tau1 = 2
+ tau2 = 2
+ mu = 1.1
+ nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2)
+
+
+def test_mu_too_small():
+ with pytest.raises(nx.NetworkXError, match="mu must be in the interval \\[0, 1\\]"):
+ n = 100
+ tau1 = 2
+ tau2 = 2
+ mu = -1
+ nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2)
+
+
+def test_both_degrees_none():
+ with pytest.raises(
+ nx.NetworkXError,
+ match="Must assign exactly one of min_degree and average_degree",
+ ):
+ n = 100
+ tau1 = 2
+ tau2 = 2
+ mu = 1
+ nx.LFR_benchmark_graph(n, tau1, tau2, mu)
+
+
+def test_neither_degrees_none():
+ with pytest.raises(
+ nx.NetworkXError,
+ match="Must assign exactly one of min_degree and average_degree",
+ ):
+ n = 100
+ tau1 = 2
+ tau2 = 2
+ mu = 1
+ nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2, average_degree=5)
+
+
+def test_max_iters_exceeded():
+ with pytest.raises(
+ nx.ExceededMaxIterations,
+ match="Could not assign communities; try increasing min_community",
+ ):
+ n = 10
+ tau1 = 2
+ tau2 = 2
+ mu = 0.1
+ nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2, max_iters=10, seed=1)
+
+
+def test_max_deg_out_of_range():
+ with pytest.raises(
+ nx.NetworkXError, match="max_degree must be in the interval \\(0, n\\]"
+ ):
+ n = 10
+ tau1 = 2
+ tau2 = 2
+ mu = 0.1
+ nx.LFR_benchmark_graph(
+ n, tau1, tau2, mu, max_degree=n + 1, max_iters=10, seed=1
+ )
+
+
+def test_max_community():
+ n = 250
+ tau1 = 3
+ tau2 = 1.5
+ mu = 0.1
+ G = nx.LFR_benchmark_graph(
+ n,
+ tau1,
+ tau2,
+ mu,
+ average_degree=5,
+ max_degree=100,
+ min_community=50,
+ max_community=200,
+ seed=10,
+ )
+ assert len(G) == 250
+ C = {frozenset(G.nodes[v]["community"]) for v in G}
+ assert nx.community.is_partition(G.nodes(), C)
+
+
+def test_powerlaw_iterations_exceeded():
+ with pytest.raises(
+ nx.ExceededMaxIterations, match="Could not create power law sequence"
+ ):
+ n = 100
+ tau1 = 2
+ tau2 = 2
+ mu = 1
+ nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2, max_iters=0)
+
+
+def test_no_scipy_zeta():
+ zeta2 = 1.6449340668482264
+ assert abs(zeta2 - nx.generators.community._hurwitz_zeta(2, 1, 0.0001)) < 0.01
+
+
+def test_generate_min_degree_itr():
+ with pytest.raises(
+ nx.ExceededMaxIterations, match="Could not match average_degree"
+ ):
+ nx.generators.community._generate_min_degree(2, 2, 1, 0.01, 0)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_degree_seq.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_degree_seq.py
new file mode 100644
index 00000000..39ed59a5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_degree_seq.py
@@ -0,0 +1,230 @@
+import pytest
+
+import networkx as nx
+
+
+class TestConfigurationModel:
+ """Unit tests for the :func:`~networkx.configuration_model`
+ function.
+
+ """
+
+ def test_empty_degree_sequence(self):
+ """Tests that an empty degree sequence yields the null graph."""
+ G = nx.configuration_model([])
+ assert len(G) == 0
+
+ def test_degree_zero(self):
+ """Tests that a degree sequence of all zeros yields the empty
+ graph.
+
+ """
+ G = nx.configuration_model([0, 0, 0])
+ assert len(G) == 3
+ assert G.number_of_edges() == 0
+
+ def test_degree_sequence(self):
+ """Tests that the degree sequence of the generated graph matches
+ the input degree sequence.
+
+ """
+ deg_seq = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
+ G = nx.configuration_model(deg_seq, seed=12345678)
+ assert sorted((d for n, d in G.degree()), reverse=True) == [
+ 5,
+ 3,
+ 3,
+ 3,
+ 3,
+ 2,
+ 2,
+ 2,
+ 1,
+ 1,
+ 1,
+ ]
+ assert sorted((d for n, d in G.degree(range(len(deg_seq)))), reverse=True) == [
+ 5,
+ 3,
+ 3,
+ 3,
+ 3,
+ 2,
+ 2,
+ 2,
+ 1,
+ 1,
+ 1,
+ ]
+
+ def test_random_seed(self):
+ """Tests that each call with the same random seed generates the
+ same graph.
+
+ """
+ deg_seq = [3] * 12
+ G1 = nx.configuration_model(deg_seq, seed=1000)
+ G2 = nx.configuration_model(deg_seq, seed=1000)
+ assert nx.is_isomorphic(G1, G2)
+ G1 = nx.configuration_model(deg_seq, seed=10)
+ G2 = nx.configuration_model(deg_seq, seed=10)
+ assert nx.is_isomorphic(G1, G2)
+
+ def test_directed_disallowed(self):
+ """Tests that attempting to create a configuration model graph
+ using a directed graph yields an exception.
+
+ """
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.configuration_model([], create_using=nx.DiGraph())
+
+ def test_odd_degree_sum(self):
+ """Tests that a degree sequence whose sum is odd yields an
+ exception.
+
+ """
+ with pytest.raises(nx.NetworkXError):
+ nx.configuration_model([1, 2])
+
+
+def test_directed_configuration_raise_unequal():
+ with pytest.raises(nx.NetworkXError):
+ zin = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1]
+ zout = [5, 3, 3, 3, 3, 2, 2, 2, 1, 2]
+ nx.directed_configuration_model(zin, zout)
+
+
+def test_directed_configuration_model():
+ G = nx.directed_configuration_model([], [], seed=0)
+ assert len(G) == 0
+
+
+def test_simple_directed_configuration_model():
+ G = nx.directed_configuration_model([1, 1], [1, 1], seed=0)
+ assert len(G) == 2
+
+
+def test_expected_degree_graph_empty():
+ # empty graph has empty degree sequence
+ deg_seq = []
+ G = nx.expected_degree_graph(deg_seq)
+ assert dict(G.degree()) == {}
+
+
+def test_expected_degree_graph():
+ # test that fixed seed delivers the same graph
+ deg_seq = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
+ G1 = nx.expected_degree_graph(deg_seq, seed=1000)
+ assert len(G1) == 12
+
+ G2 = nx.expected_degree_graph(deg_seq, seed=1000)
+ assert nx.is_isomorphic(G1, G2)
+
+ G1 = nx.expected_degree_graph(deg_seq, seed=10)
+ G2 = nx.expected_degree_graph(deg_seq, seed=10)
+ assert nx.is_isomorphic(G1, G2)
+
+
+def test_expected_degree_graph_selfloops():
+ deg_seq = [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
+ G1 = nx.expected_degree_graph(deg_seq, seed=1000, selfloops=False)
+ G2 = nx.expected_degree_graph(deg_seq, seed=1000, selfloops=False)
+ assert nx.is_isomorphic(G1, G2)
+ assert len(G1) == 12
+
+
+def test_expected_degree_graph_skew():
+ deg_seq = [10, 2, 2, 2, 2]
+ G1 = nx.expected_degree_graph(deg_seq, seed=1000)
+ G2 = nx.expected_degree_graph(deg_seq, seed=1000)
+ assert nx.is_isomorphic(G1, G2)
+ assert len(G1) == 5
+
+
+def test_havel_hakimi_construction():
+ G = nx.havel_hakimi_graph([])
+ assert len(G) == 0
+
+ z = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
+ pytest.raises(nx.NetworkXError, nx.havel_hakimi_graph, z)
+ z = ["A", 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
+ pytest.raises(nx.NetworkXError, nx.havel_hakimi_graph, z)
+
+ z = [5, 4, 3, 3, 3, 2, 2, 2]
+ G = nx.havel_hakimi_graph(z)
+ G = nx.configuration_model(z)
+ z = [6, 5, 4, 4, 2, 1, 1, 1]
+ pytest.raises(nx.NetworkXError, nx.havel_hakimi_graph, z)
+
+ z = [10, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2]
+
+ G = nx.havel_hakimi_graph(z)
+
+ pytest.raises(nx.NetworkXError, nx.havel_hakimi_graph, z, create_using=nx.DiGraph())
+
+
+def test_directed_havel_hakimi():
+ # Test range of valid directed degree sequences
+ n, r = 100, 10
+ p = 1.0 / r
+ for i in range(r):
+ G1 = nx.erdos_renyi_graph(n, p * (i + 1), None, True)
+ din1 = [d for n, d in G1.in_degree()]
+ dout1 = [d for n, d in G1.out_degree()]
+ G2 = nx.directed_havel_hakimi_graph(din1, dout1)
+ din2 = [d for n, d in G2.in_degree()]
+ dout2 = [d for n, d in G2.out_degree()]
+ assert sorted(din1) == sorted(din2)
+ assert sorted(dout1) == sorted(dout2)
+
+ # Test non-graphical sequence
+ dout = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1]
+ din = [103, 102, 102, 102, 102, 102, 102, 102, 102, 102]
+ pytest.raises(nx.exception.NetworkXError, nx.directed_havel_hakimi_graph, din, dout)
+ # Test valid sequences
+ dout = [1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
+ din = [2, 2, 2, 2, 2, 2, 2, 2, 0, 2]
+ G2 = nx.directed_havel_hakimi_graph(din, dout)
+ dout2 = (d for n, d in G2.out_degree())
+ din2 = (d for n, d in G2.in_degree())
+ assert sorted(dout) == sorted(dout2)
+ assert sorted(din) == sorted(din2)
+ # Test unequal sums
+ din = [2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
+ pytest.raises(nx.exception.NetworkXError, nx.directed_havel_hakimi_graph, din, dout)
+ # Test for negative values
+ din = [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -2]
+ pytest.raises(nx.exception.NetworkXError, nx.directed_havel_hakimi_graph, din, dout)
+
+
+def test_degree_sequence_tree():
+ z = [1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
+ G = nx.degree_sequence_tree(z)
+ assert len(G) == len(z)
+ assert len(list(G.edges())) == sum(z) / 2
+
+ pytest.raises(
+ nx.NetworkXError, nx.degree_sequence_tree, z, create_using=nx.DiGraph()
+ )
+
+ z = [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
+ pytest.raises(nx.NetworkXError, nx.degree_sequence_tree, z)
+
+
+def test_random_degree_sequence_graph():
+ d = [1, 2, 2, 3]
+ G = nx.random_degree_sequence_graph(d, seed=42)
+ assert d == sorted(d for n, d in G.degree())
+
+
+def test_random_degree_sequence_graph_raise():
+ z = [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4]
+ pytest.raises(nx.NetworkXUnfeasible, nx.random_degree_sequence_graph, z)
+
+
+def test_random_degree_sequence_large():
+ G1 = nx.fast_gnp_random_graph(100, 0.1, seed=42)
+ d1 = (d for n, d in G1.degree())
+ G2 = nx.random_degree_sequence_graph(d1, seed=42)
+ d2 = (d for n, d in G2.degree())
+ assert sorted(d1) == sorted(d2)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_directed.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_directed.py
new file mode 100644
index 00000000..8078d9f7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_directed.py
@@ -0,0 +1,163 @@
+"""Generators - Directed Graphs
+----------------------------
+"""
+
+import pytest
+
+import networkx as nx
+from networkx.classes import Graph, MultiDiGraph
+from networkx.generators.directed import (
+ gn_graph,
+ gnc_graph,
+ gnr_graph,
+ random_k_out_graph,
+ random_uniform_k_out_graph,
+ scale_free_graph,
+)
+
+
+class TestGeneratorsDirected:
+ def test_smoke_test_random_graphs(self):
+ gn_graph(100)
+ gnr_graph(100, 0.5)
+ gnc_graph(100)
+ scale_free_graph(100)
+
+ gn_graph(100, seed=42)
+ gnr_graph(100, 0.5, seed=42)
+ gnc_graph(100, seed=42)
+ scale_free_graph(100, seed=42)
+
+ def test_create_using_keyword_arguments(self):
+ pytest.raises(nx.NetworkXError, gn_graph, 100, create_using=Graph())
+ pytest.raises(nx.NetworkXError, gnr_graph, 100, 0.5, create_using=Graph())
+ pytest.raises(nx.NetworkXError, gnc_graph, 100, create_using=Graph())
+ G = gn_graph(100, seed=1)
+ MG = gn_graph(100, create_using=MultiDiGraph(), seed=1)
+ assert sorted(G.edges()) == sorted(MG.edges())
+ G = gnr_graph(100, 0.5, seed=1)
+ MG = gnr_graph(100, 0.5, create_using=MultiDiGraph(), seed=1)
+ assert sorted(G.edges()) == sorted(MG.edges())
+ G = gnc_graph(100, seed=1)
+ MG = gnc_graph(100, create_using=MultiDiGraph(), seed=1)
+ assert sorted(G.edges()) == sorted(MG.edges())
+
+ G = scale_free_graph(
+ 100,
+ alpha=0.3,
+ beta=0.4,
+ gamma=0.3,
+ delta_in=0.3,
+ delta_out=0.1,
+ initial_graph=nx.cycle_graph(4, create_using=MultiDiGraph),
+ seed=1,
+ )
+ pytest.raises(ValueError, scale_free_graph, 100, 0.5, 0.4, 0.3)
+ pytest.raises(ValueError, scale_free_graph, 100, alpha=-0.3)
+ pytest.raises(ValueError, scale_free_graph, 100, beta=-0.3)
+ pytest.raises(ValueError, scale_free_graph, 100, gamma=-0.3)
+
+ def test_parameters(self):
+ G = nx.DiGraph()
+ G.add_node(0)
+
+ def kernel(x):
+ return x
+
+ assert nx.is_isomorphic(gn_graph(1), G)
+ assert nx.is_isomorphic(gn_graph(1, kernel=kernel), G)
+ assert nx.is_isomorphic(gnc_graph(1), G)
+ assert nx.is_isomorphic(gnr_graph(1, 0.5), G)
+
+
+def test_scale_free_graph_negative_delta():
+ with pytest.raises(ValueError, match="delta_in must be >= 0."):
+ scale_free_graph(10, delta_in=-1)
+ with pytest.raises(ValueError, match="delta_out must be >= 0."):
+ scale_free_graph(10, delta_out=-1)
+
+
+def test_non_numeric_ordering():
+ G = MultiDiGraph([("a", "b"), ("b", "c"), ("c", "a")])
+ s = scale_free_graph(3, initial_graph=G)
+ assert len(s) == 3
+ assert len(s.edges) == 3
+
+
+@pytest.mark.parametrize("ig", (nx.Graph(), nx.DiGraph([(0, 1)])))
+def test_scale_free_graph_initial_graph_kwarg(ig):
+ with pytest.raises(nx.NetworkXError):
+ scale_free_graph(100, initial_graph=ig)
+
+
+class TestRandomKOutGraph:
+ """Unit tests for the
+ :func:`~networkx.generators.directed.random_k_out_graph` function.
+
+ """
+
+ def test_regularity(self):
+ """Tests that the generated graph is `k`-out-regular."""
+ n = 10
+ k = 3
+ alpha = 1
+ G = random_k_out_graph(n, k, alpha)
+ assert all(d == k for v, d in G.out_degree())
+ G = random_k_out_graph(n, k, alpha, seed=42)
+ assert all(d == k for v, d in G.out_degree())
+
+ def test_no_self_loops(self):
+ """Tests for forbidding self-loops."""
+ n = 10
+ k = 3
+ alpha = 1
+ G = random_k_out_graph(n, k, alpha, self_loops=False)
+ assert nx.number_of_selfloops(G) == 0
+
+ def test_negative_alpha(self):
+ with pytest.raises(ValueError, match="alpha must be positive"):
+ random_k_out_graph(10, 3, -1)
+
+
+class TestUniformRandomKOutGraph:
+ """Unit tests for the
+ :func:`~networkx.generators.directed.random_uniform_k_out_graph`
+ function.
+
+ """
+
+ def test_regularity(self):
+ """Tests that the generated graph is `k`-out-regular."""
+ n = 10
+ k = 3
+ G = random_uniform_k_out_graph(n, k)
+ assert all(d == k for v, d in G.out_degree())
+ G = random_uniform_k_out_graph(n, k, seed=42)
+ assert all(d == k for v, d in G.out_degree())
+
+ def test_no_self_loops(self):
+ """Tests for forbidding self-loops."""
+ n = 10
+ k = 3
+ G = random_uniform_k_out_graph(n, k, self_loops=False)
+ assert nx.number_of_selfloops(G) == 0
+ assert all(d == k for v, d in G.out_degree())
+
+ def test_with_replacement(self):
+ n = 10
+ k = 3
+ G = random_uniform_k_out_graph(n, k, with_replacement=True)
+ assert G.is_multigraph()
+ assert all(d == k for v, d in G.out_degree())
+ n = 10
+ k = 9
+ G = random_uniform_k_out_graph(n, k, with_replacement=False, self_loops=False)
+ assert nx.number_of_selfloops(G) == 0
+ assert all(d == k for v, d in G.out_degree())
+
+ def test_without_replacement(self):
+ n = 10
+ k = 3
+ G = random_uniform_k_out_graph(n, k, with_replacement=False)
+ assert not G.is_multigraph()
+ assert all(d == k for v, d in G.out_degree())
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_duplication.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_duplication.py
new file mode 100644
index 00000000..9b6100b7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_duplication.py
@@ -0,0 +1,103 @@
+"""Unit tests for the :mod:`networkx.generators.duplication` module."""
+
+import pytest
+
+import networkx as nx
+
+
+class TestDuplicationDivergenceGraph:
+ """Unit tests for the
+ :func:`networkx.generators.duplication.duplication_divergence_graph`
+ function.
+
+ """
+
+ def test_final_size(self):
+ G = nx.duplication_divergence_graph(3, p=1)
+ assert len(G) == 3
+ G = nx.duplication_divergence_graph(3, p=1, seed=42)
+ assert len(G) == 3
+
+ def test_probability_too_large(self):
+ with pytest.raises(nx.NetworkXError):
+ nx.duplication_divergence_graph(3, p=2)
+
+ def test_probability_too_small(self):
+ with pytest.raises(nx.NetworkXError):
+ nx.duplication_divergence_graph(3, p=-1)
+
+ def test_non_extreme_probability_value(self):
+ G = nx.duplication_divergence_graph(6, p=0.3, seed=42)
+ assert len(G) == 6
+ assert list(G.degree()) == [(0, 2), (1, 3), (2, 2), (3, 3), (4, 1), (5, 1)]
+
+ def test_minimum_desired_nodes(self):
+ with pytest.raises(
+ nx.NetworkXError, match=".*n must be greater than or equal to 2"
+ ):
+ nx.duplication_divergence_graph(1, p=1)
+
+ def test_create_using(self):
+ class DummyGraph(nx.Graph):
+ pass
+
+ class DummyDiGraph(nx.DiGraph):
+ pass
+
+ G = nx.duplication_divergence_graph(6, 0.3, seed=42, create_using=DummyGraph)
+ assert isinstance(G, DummyGraph)
+ with pytest.raises(nx.NetworkXError, match="create_using must not be directed"):
+ nx.duplication_divergence_graph(6, 0.3, seed=42, create_using=DummyDiGraph)
+
+
+class TestPartialDuplicationGraph:
+ """Unit tests for the
+ :func:`networkx.generators.duplication.partial_duplication_graph`
+ function.
+
+ """
+
+ def test_final_size(self):
+ N = 10
+ n = 5
+ p = 0.5
+ q = 0.5
+ G = nx.partial_duplication_graph(N, n, p, q)
+ assert len(G) == N
+ G = nx.partial_duplication_graph(N, n, p, q, seed=42)
+ assert len(G) == N
+
+ def test_initial_clique_size(self):
+ N = 10
+ n = 10
+ p = 0.5
+ q = 0.5
+ G = nx.partial_duplication_graph(N, n, p, q)
+ assert len(G) == n
+
+ def test_invalid_initial_size(self):
+ with pytest.raises(nx.NetworkXError):
+ N = 5
+ n = 10
+ p = 0.5
+ q = 0.5
+ G = nx.partial_duplication_graph(N, n, p, q)
+
+ def test_invalid_probabilities(self):
+ N = 1
+ n = 1
+ for p, q in [(0.5, 2), (0.5, -1), (2, 0.5), (-1, 0.5)]:
+ args = (N, n, p, q)
+ pytest.raises(nx.NetworkXError, nx.partial_duplication_graph, *args)
+
+ def test_create_using(self):
+ class DummyGraph(nx.Graph):
+ pass
+
+ class DummyDiGraph(nx.DiGraph):
+ pass
+
+ G = nx.partial_duplication_graph(10, 5, 0.5, 0.5, create_using=DummyGraph)
+ assert isinstance(G, DummyGraph)
+ with pytest.raises(nx.NetworkXError, match="create_using must not be directed"):
+ nx.partial_duplication_graph(10, 5, 0.5, 0.5, create_using=DummyDiGraph)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_ego.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_ego.py
new file mode 100644
index 00000000..f6fc7795
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_ego.py
@@ -0,0 +1,39 @@
+"""
+ego graph
+---------
+"""
+
+import networkx as nx
+from networkx.utils import edges_equal, nodes_equal
+
+
+class TestGeneratorEgo:
+ def test_ego(self):
+ G = nx.star_graph(3)
+ H = nx.ego_graph(G, 0)
+ assert nx.is_isomorphic(G, H)
+ G.add_edge(1, 11)
+ G.add_edge(2, 22)
+ G.add_edge(3, 33)
+ H = nx.ego_graph(G, 0)
+ assert nx.is_isomorphic(nx.star_graph(3), H)
+ G = nx.path_graph(3)
+ H = nx.ego_graph(G, 0)
+ assert edges_equal(H.edges(), [(0, 1)])
+ H = nx.ego_graph(G, 0, undirected=True)
+ assert edges_equal(H.edges(), [(0, 1)])
+ H = nx.ego_graph(G, 0, center=False)
+ assert edges_equal(H.edges(), [])
+
+ def test_ego_distance(self):
+ G = nx.Graph()
+ G.add_edge(0, 1, weight=2, distance=1)
+ G.add_edge(1, 2, weight=2, distance=2)
+ G.add_edge(2, 3, weight=2, distance=1)
+ assert nodes_equal(nx.ego_graph(G, 0, radius=3).nodes(), [0, 1, 2, 3])
+ eg = nx.ego_graph(G, 0, radius=3, distance="weight")
+ assert nodes_equal(eg.nodes(), [0, 1])
+ eg = nx.ego_graph(G, 0, radius=3, distance="weight", undirected=True)
+ assert nodes_equal(eg.nodes(), [0, 1])
+ eg = nx.ego_graph(G, 0, radius=3, distance="distance")
+ assert nodes_equal(eg.nodes(), [0, 1, 2])
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_expanders.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_expanders.py
new file mode 100644
index 00000000..7cebc588
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_expanders.py
@@ -0,0 +1,162 @@
+"""Unit tests for the :mod:`networkx.generators.expanders` module."""
+
+import pytest
+
+import networkx as nx
+
+
+@pytest.mark.parametrize("n", (2, 3, 5, 6, 10))
+def test_margulis_gabber_galil_graph_properties(n):
+ g = nx.margulis_gabber_galil_graph(n)
+ assert g.number_of_nodes() == n * n
+ for node in g:
+ assert g.degree(node) == 8
+ assert len(node) == 2
+ for i in node:
+ assert int(i) == i
+ assert 0 <= i < n
+
+
+@pytest.mark.parametrize("n", (2, 3, 5, 6, 10))
+def test_margulis_gabber_galil_graph_eigvals(n):
+ np = pytest.importorskip("numpy")
+ sp = pytest.importorskip("scipy")
+
+ g = nx.margulis_gabber_galil_graph(n)
+ # Eigenvalues are already sorted using the scipy eigvalsh,
+ # but the implementation in numpy does not guarantee order.
+ w = sorted(sp.linalg.eigvalsh(nx.adjacency_matrix(g).toarray()))
+ assert w[-2] < 5 * np.sqrt(2)
+
+
+@pytest.mark.parametrize("p", (3, 5, 7, 11)) # Primes
+def test_chordal_cycle_graph(p):
+ """Test for the :func:`networkx.chordal_cycle_graph` function."""
+ G = nx.chordal_cycle_graph(p)
+ assert len(G) == p
+ # TODO The second largest eigenvalue should be smaller than a constant,
+ # independent of the number of nodes in the graph:
+ #
+ # eigs = sorted(sp.linalg.eigvalsh(nx.adjacency_matrix(G).toarray()))
+ # assert_less(eigs[-2], ...)
+ #
+
+
+@pytest.mark.parametrize("p", (3, 5, 7, 11, 13)) # Primes
+def test_paley_graph(p):
+ """Test for the :func:`networkx.paley_graph` function."""
+ G = nx.paley_graph(p)
+ # G has p nodes
+ assert len(G) == p
+ # G is (p-1)/2-regular
+ in_degrees = {G.in_degree(node) for node in G.nodes}
+ out_degrees = {G.out_degree(node) for node in G.nodes}
+ assert len(in_degrees) == 1 and in_degrees.pop() == (p - 1) // 2
+ assert len(out_degrees) == 1 and out_degrees.pop() == (p - 1) // 2
+
+ # If p = 1 mod 4, -1 is a square mod 4 and therefore the
+ # edge in the Paley graph are symmetric.
+ if p % 4 == 1:
+ for u, v in G.edges:
+ assert (v, u) in G.edges
+
+
+@pytest.mark.parametrize("d, n", [(2, 7), (4, 10), (4, 16)])
+def test_maybe_regular_expander(d, n):
+ pytest.importorskip("numpy")
+ G = nx.maybe_regular_expander(n, d)
+
+ assert len(G) == n, "Should have n nodes"
+ assert len(G.edges) == n * d / 2, "Should have n*d/2 edges"
+ assert nx.is_k_regular(G, d), "Should be d-regular"
+
+
+@pytest.mark.parametrize("n", (3, 5, 6, 10))
+def test_is_regular_expander(n):
+ pytest.importorskip("numpy")
+ pytest.importorskip("scipy")
+ G = nx.complete_graph(n)
+
+ assert nx.is_regular_expander(G) == True, "Should be a regular expander"
+
+
+@pytest.mark.parametrize("d, n", [(2, 7), (4, 10), (4, 16)])
+def test_random_regular_expander(d, n):
+ pytest.importorskip("numpy")
+ pytest.importorskip("scipy")
+ G = nx.random_regular_expander_graph(n, d)
+
+ assert len(G) == n, "Should have n nodes"
+ assert len(G.edges) == n * d / 2, "Should have n*d/2 edges"
+ assert nx.is_k_regular(G, d), "Should be d-regular"
+ assert nx.is_regular_expander(G) == True, "Should be a regular expander"
+
+
+def test_random_regular_expander_explicit_construction():
+ pytest.importorskip("numpy")
+ pytest.importorskip("scipy")
+ G = nx.random_regular_expander_graph(d=4, n=5)
+
+ assert len(G) == 5 and len(G.edges) == 10, "Should be a complete graph"
+
+
+@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph, nx.MultiDiGraph))
+def test_margulis_gabber_galil_graph_badinput(graph_type):
+ with pytest.raises(
+ nx.NetworkXError, match="`create_using` must be an undirected multigraph"
+ ):
+ nx.margulis_gabber_galil_graph(3, create_using=graph_type)
+
+
+@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph, nx.MultiDiGraph))
+def test_chordal_cycle_graph_badinput(graph_type):
+ with pytest.raises(
+ nx.NetworkXError, match="`create_using` must be an undirected multigraph"
+ ):
+ nx.chordal_cycle_graph(3, create_using=graph_type)
+
+
+def test_paley_graph_badinput():
+ with pytest.raises(
+ nx.NetworkXError, match="`create_using` cannot be a multigraph."
+ ):
+ nx.paley_graph(3, create_using=nx.MultiGraph)
+
+
+def test_maybe_regular_expander_badinput():
+ pytest.importorskip("numpy")
+ pytest.importorskip("scipy")
+
+ with pytest.raises(nx.NetworkXError, match="n must be a positive integer"):
+ nx.maybe_regular_expander(n=-1, d=2)
+
+ with pytest.raises(nx.NetworkXError, match="d must be greater than or equal to 2"):
+ nx.maybe_regular_expander(n=10, d=0)
+
+ with pytest.raises(nx.NetworkXError, match="Need n-1>= d to have room"):
+ nx.maybe_regular_expander(n=5, d=6)
+
+
+def test_is_regular_expander_badinput():
+ pytest.importorskip("numpy")
+ pytest.importorskip("scipy")
+
+ with pytest.raises(nx.NetworkXError, match="epsilon must be non negative"):
+ nx.is_regular_expander(nx.Graph(), epsilon=-1)
+
+
+def test_random_regular_expander_badinput():
+ pytest.importorskip("numpy")
+ pytest.importorskip("scipy")
+
+ with pytest.raises(nx.NetworkXError, match="n must be a positive integer"):
+ nx.random_regular_expander_graph(n=-1, d=2)
+
+ with pytest.raises(nx.NetworkXError, match="d must be greater than or equal to 2"):
+ nx.random_regular_expander_graph(n=10, d=0)
+
+ with pytest.raises(nx.NetworkXError, match="Need n-1>= d to have room"):
+ nx.random_regular_expander_graph(n=5, d=6)
+
+ with pytest.raises(nx.NetworkXError, match="epsilon must be non negative"):
+ nx.random_regular_expander_graph(n=4, d=2, epsilon=-1)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_geometric.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_geometric.py
new file mode 100644
index 00000000..f1c68bea
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_geometric.py
@@ -0,0 +1,488 @@
+import math
+import random
+from itertools import combinations
+
+import pytest
+
+import networkx as nx
+
+
+def l1dist(x, y):
+ return sum(abs(a - b) for a, b in zip(x, y))
+
+
+class TestRandomGeometricGraph:
+ """Unit tests for :func:`~networkx.random_geometric_graph`"""
+
+ def test_number_of_nodes(self):
+ G = nx.random_geometric_graph(50, 0.25, seed=42)
+ assert len(G) == 50
+ G = nx.random_geometric_graph(range(50), 0.25, seed=42)
+ assert len(G) == 50
+
+ def test_distances(self):
+ """Tests that pairs of vertices adjacent if and only if they are
+ within the prescribed radius.
+ """
+ # Use the Euclidean metric, the default according to the
+ # documentation.
+ G = nx.random_geometric_graph(50, 0.25)
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+ # Nonadjacent vertices must be at greater distance.
+ else:
+ assert not math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+
+ def test_p(self):
+ """Tests for providing an alternate distance metric to the generator."""
+ # Use the L1 metric.
+ G = nx.random_geometric_graph(50, 0.25, p=1)
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert l1dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+ # Nonadjacent vertices must be at greater distance.
+ else:
+ assert not l1dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+
+ def test_node_names(self):
+ """Tests using values other than sequential numbers as node IDs."""
+ import string
+
+ nodes = list(string.ascii_lowercase)
+ G = nx.random_geometric_graph(nodes, 0.25)
+ assert len(G) == len(nodes)
+
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+ # Nonadjacent vertices must be at greater distance.
+ else:
+ assert not math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+
+ def test_pos_name(self):
+ G = nx.random_geometric_graph(50, 0.25, seed=42, pos_name="coords")
+ assert all(len(d["coords"]) == 2 for n, d in G.nodes.items())
+
+
+class TestSoftRandomGeometricGraph:
+ """Unit tests for :func:`~networkx.soft_random_geometric_graph`"""
+
+ def test_number_of_nodes(self):
+ G = nx.soft_random_geometric_graph(50, 0.25, seed=42)
+ assert len(G) == 50
+ G = nx.soft_random_geometric_graph(range(50), 0.25, seed=42)
+ assert len(G) == 50
+
+ def test_distances(self):
+ """Tests that pairs of vertices adjacent if and only if they are
+ within the prescribed radius.
+ """
+ # Use the Euclidean metric, the default according to the
+ # documentation.
+ G = nx.soft_random_geometric_graph(50, 0.25)
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+
+ def test_p(self):
+ """Tests for providing an alternate distance metric to the generator."""
+
+ # Use the L1 metric.
+ def dist(x, y):
+ return sum(abs(a - b) for a, b in zip(x, y))
+
+ G = nx.soft_random_geometric_graph(50, 0.25, p=1)
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+
+ def test_node_names(self):
+ """Tests using values other than sequential numbers as node IDs."""
+ import string
+
+ nodes = list(string.ascii_lowercase)
+ G = nx.soft_random_geometric_graph(nodes, 0.25)
+ assert len(G) == len(nodes)
+
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+
+ def test_p_dist_default(self):
+ """Tests default p_dict = 0.5 returns graph with edge count <= RGG with
+ same n, radius, dim and positions
+ """
+ nodes = 50
+ dim = 2
+ pos = {v: [random.random() for i in range(dim)] for v in range(nodes)}
+ RGG = nx.random_geometric_graph(50, 0.25, pos=pos)
+ SRGG = nx.soft_random_geometric_graph(50, 0.25, pos=pos)
+ assert len(SRGG.edges()) <= len(RGG.edges())
+
+ def test_p_dist_zero(self):
+ """Tests if p_dict = 0 returns disconnected graph with 0 edges"""
+
+ def p_dist(dist):
+ return 0
+
+ G = nx.soft_random_geometric_graph(50, 0.25, p_dist=p_dist)
+ assert len(G.edges) == 0
+
+ def test_pos_name(self):
+ G = nx.soft_random_geometric_graph(50, 0.25, seed=42, pos_name="coords")
+ assert all(len(d["coords"]) == 2 for n, d in G.nodes.items())
+
+
+def join(G, u, v, theta, alpha, metric):
+ """Returns ``True`` if and only if the nodes whose attributes are
+ ``du`` and ``dv`` should be joined, according to the threshold
+ condition for geographical threshold graphs.
+
+ ``G`` is an undirected NetworkX graph, and ``u`` and ``v`` are nodes
+ in that graph. The nodes must have node attributes ``'pos'`` and
+ ``'weight'``.
+
+ ``metric`` is a distance metric.
+ """
+ du, dv = G.nodes[u], G.nodes[v]
+ u_pos, v_pos = du["pos"], dv["pos"]
+ u_weight, v_weight = du["weight"], dv["weight"]
+ return (u_weight + v_weight) * metric(u_pos, v_pos) ** alpha >= theta
+
+
+class TestGeographicalThresholdGraph:
+ """Unit tests for :func:`~networkx.geographical_threshold_graph`"""
+
+ def test_number_of_nodes(self):
+ G = nx.geographical_threshold_graph(50, 100, seed=42)
+ assert len(G) == 50
+ G = nx.geographical_threshold_graph(range(50), 100, seed=42)
+ assert len(G) == 50
+
+ def test_distances(self):
+ """Tests that pairs of vertices adjacent if and only if their
+ distances meet the given threshold.
+ """
+ # Use the Euclidean metric and alpha = -2
+ # the default according to the documentation.
+ G = nx.geographical_threshold_graph(50, 10)
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must exceed the threshold.
+ if v in G[u]:
+ assert join(G, u, v, 10, -2, math.dist)
+ # Nonadjacent vertices must not exceed the threshold.
+ else:
+ assert not join(G, u, v, 10, -2, math.dist)
+
+ def test_metric(self):
+ """Tests for providing an alternate distance metric to the generator."""
+ # Use the L1 metric.
+ G = nx.geographical_threshold_graph(50, 10, metric=l1dist)
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must exceed the threshold.
+ if v in G[u]:
+ assert join(G, u, v, 10, -2, l1dist)
+ # Nonadjacent vertices must not exceed the threshold.
+ else:
+ assert not join(G, u, v, 10, -2, l1dist)
+
+ def test_p_dist_zero(self):
+ """Tests if p_dict = 0 returns disconnected graph with 0 edges"""
+
+ def p_dist(dist):
+ return 0
+
+ G = nx.geographical_threshold_graph(50, 1, p_dist=p_dist)
+ assert len(G.edges) == 0
+
+ def test_pos_weight_name(self):
+ gtg = nx.geographical_threshold_graph
+ G = gtg(50, 100, seed=42, pos_name="coords", weight_name="wt")
+ assert all(len(d["coords"]) == 2 for n, d in G.nodes.items())
+ assert all(d["wt"] > 0 for n, d in G.nodes.items())
+
+
+class TestWaxmanGraph:
+ """Unit tests for the :func:`~networkx.waxman_graph` function."""
+
+ def test_number_of_nodes_1(self):
+ G = nx.waxman_graph(50, 0.5, 0.1, seed=42)
+ assert len(G) == 50
+ G = nx.waxman_graph(range(50), 0.5, 0.1, seed=42)
+ assert len(G) == 50
+
+ def test_number_of_nodes_2(self):
+ G = nx.waxman_graph(50, 0.5, 0.1, L=1)
+ assert len(G) == 50
+ G = nx.waxman_graph(range(50), 0.5, 0.1, L=1)
+ assert len(G) == 50
+
+ def test_metric(self):
+ """Tests for providing an alternate distance metric to the generator."""
+ # Use the L1 metric.
+ G = nx.waxman_graph(50, 0.5, 0.1, metric=l1dist)
+ assert len(G) == 50
+
+ def test_pos_name(self):
+ G = nx.waxman_graph(50, 0.5, 0.1, seed=42, pos_name="coords")
+ assert all(len(d["coords"]) == 2 for n, d in G.nodes.items())
+
+
+class TestNavigableSmallWorldGraph:
+ def test_navigable_small_world(self):
+ G = nx.navigable_small_world_graph(5, p=1, q=0, seed=42)
+ gg = nx.grid_2d_graph(5, 5).to_directed()
+ assert nx.is_isomorphic(G, gg)
+
+ G = nx.navigable_small_world_graph(5, p=1, q=0, dim=3)
+ gg = nx.grid_graph([5, 5, 5]).to_directed()
+ assert nx.is_isomorphic(G, gg)
+
+ G = nx.navigable_small_world_graph(5, p=1, q=0, dim=1)
+ gg = nx.grid_graph([5]).to_directed()
+ assert nx.is_isomorphic(G, gg)
+
+ def test_invalid_diameter_value(self):
+ with pytest.raises(nx.NetworkXException, match=".*p must be >= 1"):
+ nx.navigable_small_world_graph(5, p=0, q=0, dim=1)
+
+ def test_invalid_long_range_connections_value(self):
+ with pytest.raises(nx.NetworkXException, match=".*q must be >= 0"):
+ nx.navigable_small_world_graph(5, p=1, q=-1, dim=1)
+
+ def test_invalid_exponent_for_decaying_probability_value(self):
+ with pytest.raises(nx.NetworkXException, match=".*r must be >= 0"):
+ nx.navigable_small_world_graph(5, p=1, q=0, r=-1, dim=1)
+
+ def test_r_between_0_and_1(self):
+ """Smoke test for radius in range [0, 1]"""
+ # q=0 means no long-range connections
+ G = nx.navigable_small_world_graph(3, p=1, q=0, r=0.5, dim=2, seed=42)
+ expected = nx.grid_2d_graph(3, 3, create_using=nx.DiGraph)
+ assert nx.utils.graphs_equal(G, expected)
+
+ @pytest.mark.parametrize("seed", range(2478, 2578, 10))
+ def test_r_general_scaling(self, seed):
+ """The probability of adding a long-range edge scales with `1 / dist**r`,
+ so a navigable_small_world graph created with r < 1 should generally
+ result in more edges than a navigable_small_world graph with r >= 1
+ (for 0 < q << n).
+
+ N.B. this is probabilistic, so this test may not hold for all seeds."""
+ G1 = nx.navigable_small_world_graph(7, q=3, r=0.5, seed=seed)
+ G2 = nx.navigable_small_world_graph(7, q=3, r=1, seed=seed)
+ G3 = nx.navigable_small_world_graph(7, q=3, r=2, seed=seed)
+ assert G1.number_of_edges() > G2.number_of_edges()
+ assert G2.number_of_edges() > G3.number_of_edges()
+
+
+class TestThresholdedRandomGeometricGraph:
+ """Unit tests for :func:`~networkx.thresholded_random_geometric_graph`"""
+
+ def test_number_of_nodes(self):
+ G = nx.thresholded_random_geometric_graph(50, 0.2, 0.1, seed=42)
+ assert len(G) == 50
+ G = nx.thresholded_random_geometric_graph(range(50), 0.2, 0.1, seed=42)
+ assert len(G) == 50
+
+ def test_distances(self):
+ """Tests that pairs of vertices adjacent if and only if they are
+ within the prescribed radius.
+ """
+ # Use the Euclidean metric, the default according to the
+ # documentation.
+ G = nx.thresholded_random_geometric_graph(50, 0.25, 0.1, seed=42)
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+
+ def test_p(self):
+ """Tests for providing an alternate distance metric to the generator."""
+
+ # Use the L1 metric.
+ def dist(x, y):
+ return sum(abs(a - b) for a, b in zip(x, y))
+
+ G = nx.thresholded_random_geometric_graph(50, 0.25, 0.1, p=1, seed=42)
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+
+ def test_node_names(self):
+ """Tests using values other than sequential numbers as node IDs."""
+ import string
+
+ nodes = list(string.ascii_lowercase)
+ G = nx.thresholded_random_geometric_graph(nodes, 0.25, 0.1, seed=42)
+ assert len(G) == len(nodes)
+
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25
+
+ def test_theta(self):
+ """Tests that pairs of vertices adjacent if and only if their sum
+ weights exceeds the threshold parameter theta.
+ """
+ G = nx.thresholded_random_geometric_graph(50, 0.25, 0.1, seed=42)
+
+ for u, v in combinations(G, 2):
+ # Adjacent vertices must be within the given distance.
+ if v in G[u]:
+ assert (G.nodes[u]["weight"] + G.nodes[v]["weight"]) >= 0.1
+
+ def test_pos_name(self):
+ trgg = nx.thresholded_random_geometric_graph
+ G = trgg(50, 0.25, 0.1, seed=42, pos_name="p", weight_name="wt")
+ assert all(len(d["p"]) == 2 for n, d in G.nodes.items())
+ assert all(d["wt"] > 0 for n, d in G.nodes.items())
+
+
+def test_geometric_edges_pos_attribute():
+ G = nx.Graph()
+ G.add_nodes_from(
+ [
+ (0, {"position": (0, 0)}),
+ (1, {"position": (0, 1)}),
+ (2, {"position": (1, 0)}),
+ ]
+ )
+ expected_edges = [(0, 1), (0, 2)]
+ assert expected_edges == nx.geometric_edges(G, radius=1, pos_name="position")
+
+
+def test_geometric_edges_raises_no_pos():
+ G = nx.path_graph(3)
+ msg = "all nodes. must have a '"
+ with pytest.raises(nx.NetworkXError, match=msg):
+ nx.geometric_edges(G, radius=1)
+
+
+def test_number_of_nodes_S1():
+ G = nx.geometric_soft_configuration_graph(
+ beta=1.5, n=100, gamma=2.7, mean_degree=10, seed=42
+ )
+ assert len(G) == 100
+
+
+def test_set_attributes_S1():
+ G = nx.geometric_soft_configuration_graph(
+ beta=1.5, n=100, gamma=2.7, mean_degree=10, seed=42
+ )
+ kappas = nx.get_node_attributes(G, "kappa")
+ assert len(kappas) == 100
+ thetas = nx.get_node_attributes(G, "theta")
+ assert len(thetas) == 100
+ radii = nx.get_node_attributes(G, "radius")
+ assert len(radii) == 100
+
+
+def test_mean_kappas_mean_degree_S1():
+ G = nx.geometric_soft_configuration_graph(
+ beta=2.5, n=50, gamma=2.7, mean_degree=10, seed=8023
+ )
+
+ kappas = nx.get_node_attributes(G, "kappa")
+ mean_kappas = sum(kappas.values()) / len(kappas)
+ assert math.fabs(mean_kappas - 10) < 0.5
+
+ degrees = dict(G.degree())
+ mean_degree = sum(degrees.values()) / len(degrees)
+ assert math.fabs(mean_degree - 10) < 1
+
+
+def test_dict_kappas_S1():
+ kappas = {i: 10 for i in range(1000)}
+ G = nx.geometric_soft_configuration_graph(beta=1, kappas=kappas)
+ assert len(G) == 1000
+ kappas = nx.get_node_attributes(G, "kappa")
+ assert all(kappa == 10 for kappa in kappas.values())
+
+
+def test_beta_clustering_S1():
+ G1 = nx.geometric_soft_configuration_graph(
+ beta=1.5, n=100, gamma=3.5, mean_degree=10, seed=42
+ )
+ G2 = nx.geometric_soft_configuration_graph(
+ beta=3.0, n=100, gamma=3.5, mean_degree=10, seed=42
+ )
+ assert nx.average_clustering(G1) < nx.average_clustering(G2)
+
+
+def test_wrong_parameters_S1():
+ with pytest.raises(
+ nx.NetworkXError,
+ match="Please provide either kappas, or all 3 of: n, gamma and mean_degree.",
+ ):
+ G = nx.geometric_soft_configuration_graph(
+ beta=1.5, gamma=3.5, mean_degree=10, seed=42
+ )
+
+ with pytest.raises(
+ nx.NetworkXError,
+ match="When kappas is input, n, gamma and mean_degree must not be.",
+ ):
+ kappas = {i: 10 for i in range(1000)}
+ G = nx.geometric_soft_configuration_graph(
+ beta=1.5, kappas=kappas, gamma=2.3, seed=42
+ )
+
+ with pytest.raises(
+ nx.NetworkXError,
+ match="Please provide either kappas, or all 3 of: n, gamma and mean_degree.",
+ ):
+ G = nx.geometric_soft_configuration_graph(beta=1.5, seed=42)
+
+
+def test_negative_beta_S1():
+ with pytest.raises(
+ nx.NetworkXError, match="The parameter beta cannot be smaller or equal to 0."
+ ):
+ G = nx.geometric_soft_configuration_graph(
+ beta=-1, n=100, gamma=2.3, mean_degree=10, seed=42
+ )
+
+
+def test_non_zero_clustering_beta_lower_one_S1():
+ G = nx.geometric_soft_configuration_graph(
+ beta=0.5, n=100, gamma=3.5, mean_degree=10, seed=42
+ )
+ assert nx.average_clustering(G) > 0
+
+
+def test_mean_degree_influence_on_connectivity_S1():
+ low_mean_degree = 2
+ high_mean_degree = 20
+ G_low = nx.geometric_soft_configuration_graph(
+ beta=1.2, n=100, gamma=2.7, mean_degree=low_mean_degree, seed=42
+ )
+ G_high = nx.geometric_soft_configuration_graph(
+ beta=1.2, n=100, gamma=2.7, mean_degree=high_mean_degree, seed=42
+ )
+ assert nx.number_connected_components(G_low) > nx.number_connected_components(
+ G_high
+ )
+
+
+def test_compare_mean_kappas_different_gammas_S1():
+ G1 = nx.geometric_soft_configuration_graph(
+ beta=1.5, n=20, gamma=2.7, mean_degree=5, seed=42
+ )
+ G2 = nx.geometric_soft_configuration_graph(
+ beta=1.5, n=20, gamma=3.5, mean_degree=5, seed=42
+ )
+ kappas1 = nx.get_node_attributes(G1, "kappa")
+ mean_kappas1 = sum(kappas1.values()) / len(kappas1)
+ kappas2 = nx.get_node_attributes(G2, "kappa")
+ mean_kappas2 = sum(kappas2.values()) / len(kappas2)
+ assert math.fabs(mean_kappas1 - mean_kappas2) < 1
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_harary_graph.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_harary_graph.py
new file mode 100644
index 00000000..8a0142df
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_harary_graph.py
@@ -0,0 +1,133 @@
+"""Unit tests for the :mod:`networkx.generators.harary_graph` module."""
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.isomorphism.isomorph import is_isomorphic
+from networkx.generators.harary_graph import hkn_harary_graph, hnm_harary_graph
+
+
+class TestHararyGraph:
+ """
+ Suppose n nodes, m >= n-1 edges, d = 2m // n, r = 2m % n
+ """
+
+ def test_hnm_harary_graph(self):
+ # When d is even and r = 0, the hnm_harary_graph(n,m) is
+ # the circulant_graph(n, list(range(1,d/2+1)))
+ for n, m in [(5, 5), (6, 12), (7, 14)]:
+ G1 = hnm_harary_graph(n, m)
+ d = 2 * m // n
+ G2 = nx.circulant_graph(n, list(range(1, d // 2 + 1)))
+ assert is_isomorphic(G1, G2)
+
+ # When d is even and r > 0, the hnm_harary_graph(n,m) is
+ # the circulant_graph(n, list(range(1,d/2+1)))
+ # with r edges added arbitrarily
+ for n, m in [(5, 7), (6, 13), (7, 16)]:
+ G1 = hnm_harary_graph(n, m)
+ d = 2 * m // n
+ G2 = nx.circulant_graph(n, list(range(1, d // 2 + 1)))
+ assert set(G2.edges) < set(G1.edges)
+ assert G1.number_of_edges() == m
+
+ # When d is odd and n is even and r = 0, the hnm_harary_graph(n,m)
+ # is the circulant_graph(n, list(range(1,(d+1)/2) plus [n//2])
+ for n, m in [(6, 9), (8, 12), (10, 15)]:
+ G1 = hnm_harary_graph(n, m)
+ d = 2 * m // n
+ L = list(range(1, (d + 1) // 2))
+ L.append(n // 2)
+ G2 = nx.circulant_graph(n, L)
+ assert is_isomorphic(G1, G2)
+
+ # When d is odd and n is even and r > 0, the hnm_harary_graph(n,m)
+ # is the circulant_graph(n, list(range(1,(d+1)/2) plus [n//2])
+ # with r edges added arbitrarily
+ for n, m in [(6, 10), (8, 13), (10, 17)]:
+ G1 = hnm_harary_graph(n, m)
+ d = 2 * m // n
+ L = list(range(1, (d + 1) // 2))
+ L.append(n // 2)
+ G2 = nx.circulant_graph(n, L)
+ assert set(G2.edges) < set(G1.edges)
+ assert G1.number_of_edges() == m
+
+ # When d is odd and n is odd, the hnm_harary_graph(n,m) is
+ # the circulant_graph(n, list(range(1,(d+1)/2))
+ # with m - n*(d-1)/2 edges added arbitrarily
+ for n, m in [(5, 4), (7, 12), (9, 14)]:
+ G1 = hnm_harary_graph(n, m)
+ d = 2 * m // n
+ L = list(range(1, (d + 1) // 2))
+ G2 = nx.circulant_graph(n, L)
+ assert set(G2.edges) < set(G1.edges)
+ assert G1.number_of_edges() == m
+
+ # Raise NetworkXError if n<1
+ n = 0
+ m = 0
+ pytest.raises(nx.NetworkXError, hnm_harary_graph, n, m)
+
+ # Raise NetworkXError if m < n-1
+ n = 6
+ m = 4
+ pytest.raises(nx.NetworkXError, hnm_harary_graph, n, m)
+
+ # Raise NetworkXError if m > n(n-1)/2
+ n = 6
+ m = 16
+ pytest.raises(nx.NetworkXError, hnm_harary_graph, n, m)
+
+ """
+ Suppose connectivity k, number of nodes n
+ """
+
+ def test_hkn_harary_graph(self):
+ # When k == 1, the hkn_harary_graph(k,n) is
+ # the path_graph(n)
+ for k, n in [(1, 6), (1, 7)]:
+ G1 = hkn_harary_graph(k, n)
+ G2 = nx.path_graph(n)
+ assert is_isomorphic(G1, G2)
+
+ # When k is even, the hkn_harary_graph(k,n) is
+ # the circulant_graph(n, list(range(1,k/2+1)))
+ for k, n in [(2, 6), (2, 7), (4, 6), (4, 7)]:
+ G1 = hkn_harary_graph(k, n)
+ G2 = nx.circulant_graph(n, list(range(1, k // 2 + 1)))
+ assert is_isomorphic(G1, G2)
+
+ # When k is odd and n is even, the hkn_harary_graph(k,n) is
+ # the circulant_graph(n, list(range(1,(k+1)/2)) plus [n/2])
+ for k, n in [(3, 6), (5, 8), (7, 10)]:
+ G1 = hkn_harary_graph(k, n)
+ L = list(range(1, (k + 1) // 2))
+ L.append(n // 2)
+ G2 = nx.circulant_graph(n, L)
+ assert is_isomorphic(G1, G2)
+
+ # When k is odd and n is odd, the hkn_harary_graph(k,n) is
+ # the circulant_graph(n, list(range(1,(k+1)/2))) with
+ # n//2+1 edges added between node i and node i+n//2+1
+ for k, n in [(3, 5), (5, 9), (7, 11)]:
+ G1 = hkn_harary_graph(k, n)
+ G2 = nx.circulant_graph(n, list(range(1, (k + 1) // 2)))
+ eSet1 = set(G1.edges)
+ eSet2 = set(G2.edges)
+ eSet3 = set()
+ half = n // 2
+ for i in range(half + 1):
+ # add half+1 edges between i and i+half
+ eSet3.add((i, (i + half) % n))
+ assert eSet1 == eSet2 | eSet3
+
+ # Raise NetworkXError if k<1
+ k = 0
+ n = 0
+ pytest.raises(nx.NetworkXError, hkn_harary_graph, k, n)
+
+ # Raise NetworkXError if n<k+1
+ k = 6
+ n = 6
+ pytest.raises(nx.NetworkXError, hkn_harary_graph, k, n)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_internet_as_graphs.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_internet_as_graphs.py
new file mode 100644
index 00000000..0d578b4b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_internet_as_graphs.py
@@ -0,0 +1,176 @@
+from pytest import approx
+
+from networkx import is_connected, neighbors
+from networkx.generators.internet_as_graphs import random_internet_as_graph
+
+
+class TestInternetASTopology:
+ @classmethod
+ def setup_class(cls):
+ cls.n = 1000
+ cls.seed = 42
+ cls.G = random_internet_as_graph(cls.n, cls.seed)
+ cls.T = []
+ cls.M = []
+ cls.C = []
+ cls.CP = []
+ cls.customers = {}
+ cls.providers = {}
+
+ for i in cls.G.nodes():
+ if cls.G.nodes[i]["type"] == "T":
+ cls.T.append(i)
+ elif cls.G.nodes[i]["type"] == "M":
+ cls.M.append(i)
+ elif cls.G.nodes[i]["type"] == "C":
+ cls.C.append(i)
+ elif cls.G.nodes[i]["type"] == "CP":
+ cls.CP.append(i)
+ else:
+ raise ValueError("Inconsistent data in the graph node attributes")
+ cls.set_customers(i)
+ cls.set_providers(i)
+
+ @classmethod
+ def set_customers(cls, i):
+ if i not in cls.customers:
+ cls.customers[i] = set()
+ for j in neighbors(cls.G, i):
+ e = cls.G.edges[(i, j)]
+ if e["type"] == "transit":
+ customer = int(e["customer"])
+ if j == customer:
+ cls.set_customers(j)
+ cls.customers[i] = cls.customers[i].union(cls.customers[j])
+ cls.customers[i].add(j)
+ elif i != customer:
+ raise ValueError(
+ "Inconsistent data in the graph edge attributes"
+ )
+
+ @classmethod
+ def set_providers(cls, i):
+ if i not in cls.providers:
+ cls.providers[i] = set()
+ for j in neighbors(cls.G, i):
+ e = cls.G.edges[(i, j)]
+ if e["type"] == "transit":
+ customer = int(e["customer"])
+ if i == customer:
+ cls.set_providers(j)
+ cls.providers[i] = cls.providers[i].union(cls.providers[j])
+ cls.providers[i].add(j)
+ elif j != customer:
+ raise ValueError(
+ "Inconsistent data in the graph edge attributes"
+ )
+
+ def test_wrong_input(self):
+ G = random_internet_as_graph(0)
+ assert len(G.nodes()) == 0
+
+ G = random_internet_as_graph(-1)
+ assert len(G.nodes()) == 0
+
+ G = random_internet_as_graph(1)
+ assert len(G.nodes()) == 1
+
+ def test_node_numbers(self):
+ assert len(self.G.nodes()) == self.n
+ assert len(self.T) < 7
+ assert len(self.M) == round(self.n * 0.15)
+ assert len(self.CP) == round(self.n * 0.05)
+ numb = self.n - len(self.T) - len(self.M) - len(self.CP)
+ assert len(self.C) == numb
+
+ def test_connectivity(self):
+ assert is_connected(self.G)
+
+ def test_relationships(self):
+ # T nodes are not customers of anyone
+ for i in self.T:
+ assert len(self.providers[i]) == 0
+
+ # C nodes are not providers of anyone
+ for i in self.C:
+ assert len(self.customers[i]) == 0
+
+ # CP nodes are not providers of anyone
+ for i in self.CP:
+ assert len(self.customers[i]) == 0
+
+ # test whether there is a customer-provider loop
+ for i in self.G.nodes():
+ assert len(self.customers[i].intersection(self.providers[i])) == 0
+
+ # test whether there is a peering with a customer or provider
+ for i, j in self.G.edges():
+ if self.G.edges[(i, j)]["type"] == "peer":
+ assert j not in self.customers[i]
+ assert i not in self.customers[j]
+ assert j not in self.providers[i]
+ assert i not in self.providers[j]
+
+ def test_degree_values(self):
+ d_m = 0 # multihoming degree for M nodes
+ d_cp = 0 # multihoming degree for CP nodes
+ d_c = 0 # multihoming degree for C nodes
+ p_m_m = 0 # avg number of peering edges between M and M
+ p_cp_m = 0 # avg number of peering edges between CP and M
+ p_cp_cp = 0 # avg number of peering edges between CP and CP
+ t_m = 0 # probability M's provider is T
+ t_cp = 0 # probability CP's provider is T
+ t_c = 0 # probability C's provider is T
+
+ for i, j in self.G.edges():
+ e = self.G.edges[(i, j)]
+ if e["type"] == "transit":
+ cust = int(e["customer"])
+ if i == cust:
+ prov = j
+ elif j == cust:
+ prov = i
+ else:
+ raise ValueError("Inconsistent data in the graph edge attributes")
+ if cust in self.M:
+ d_m += 1
+ if self.G.nodes[prov]["type"] == "T":
+ t_m += 1
+ elif cust in self.C:
+ d_c += 1
+ if self.G.nodes[prov]["type"] == "T":
+ t_c += 1
+ elif cust in self.CP:
+ d_cp += 1
+ if self.G.nodes[prov]["type"] == "T":
+ t_cp += 1
+ else:
+ raise ValueError("Inconsistent data in the graph edge attributes")
+ elif e["type"] == "peer":
+ if self.G.nodes[i]["type"] == "M" and self.G.nodes[j]["type"] == "M":
+ p_m_m += 1
+ if self.G.nodes[i]["type"] == "CP" and self.G.nodes[j]["type"] == "CP":
+ p_cp_cp += 1
+ if (
+ self.G.nodes[i]["type"] == "M"
+ and self.G.nodes[j]["type"] == "CP"
+ or self.G.nodes[i]["type"] == "CP"
+ and self.G.nodes[j]["type"] == "M"
+ ):
+ p_cp_m += 1
+ else:
+ raise ValueError("Unexpected data in the graph edge attributes")
+
+ assert d_m / len(self.M) == approx((2 + (2.5 * self.n) / 10000), abs=1e-0)
+ assert d_cp / len(self.CP) == approx((2 + (1.5 * self.n) / 10000), abs=1e-0)
+ assert d_c / len(self.C) == approx((1 + (5 * self.n) / 100000), abs=1e-0)
+
+ assert p_m_m / len(self.M) == approx((1 + (2 * self.n) / 10000), abs=1e-0)
+ assert p_cp_m / len(self.CP) == approx((0.2 + (2 * self.n) / 10000), abs=1e-0)
+ assert p_cp_cp / len(self.CP) == approx(
+ (0.05 + (2 * self.n) / 100000), abs=1e-0
+ )
+
+ assert t_m / d_m == approx(0.375, abs=1e-1)
+ assert t_cp / d_cp == approx(0.375, abs=1e-1)
+ assert t_c / d_c == approx(0.125, abs=1e-1)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_intersection.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_intersection.py
new file mode 100644
index 00000000..f52551b4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_intersection.py
@@ -0,0 +1,28 @@
+import pytest
+
+import networkx as nx
+
+
+class TestIntersectionGraph:
+ def test_random_intersection_graph(self):
+ G = nx.uniform_random_intersection_graph(10, 5, 0.5)
+ assert len(G) == 10
+
+ def test_k_random_intersection_graph(self):
+ G = nx.k_random_intersection_graph(10, 5, 2)
+ assert len(G) == 10
+
+ def test_k_random_intersection_graph_seeded(self):
+ G = nx.k_random_intersection_graph(10, 5, 2, seed=1234)
+ assert len(G) == 10
+
+ def test_general_random_intersection_graph(self):
+ G = nx.general_random_intersection_graph(10, 5, [0.1, 0.2, 0.2, 0.1, 0.1])
+ assert len(G) == 10
+ pytest.raises(
+ ValueError,
+ nx.general_random_intersection_graph,
+ 10,
+ 5,
+ [0.1, 0.2, 0.2, 0.1],
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_interval_graph.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_interval_graph.py
new file mode 100644
index 00000000..57cf7106
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_interval_graph.py
@@ -0,0 +1,144 @@
+"""Unit tests for the :mod:`networkx.generators.interval_graph` module."""
+
+import math
+
+import pytest
+
+import networkx as nx
+from networkx.generators.interval_graph import interval_graph
+from networkx.utils import edges_equal
+
+
+class TestIntervalGraph:
+ """Unit tests for :func:`networkx.generators.interval_graph.interval_graph`"""
+
+ def test_empty(self):
+ """Tests for trivial case of empty input"""
+ assert len(interval_graph([])) == 0
+
+ def test_interval_graph_check_invalid(self):
+ """Tests for conditions that raise Exceptions"""
+
+ invalids_having_none = [None, (1, 2)]
+ with pytest.raises(TypeError):
+ interval_graph(invalids_having_none)
+
+ invalids_having_set = [{1, 2}]
+ with pytest.raises(TypeError):
+ interval_graph(invalids_having_set)
+
+ invalids_having_seq_but_not_length2 = [(1, 2, 3)]
+ with pytest.raises(TypeError):
+ interval_graph(invalids_having_seq_but_not_length2)
+
+ invalids_interval = [[3, 2]]
+ with pytest.raises(ValueError):
+ interval_graph(invalids_interval)
+
+ def test_interval_graph_0(self):
+ intervals = [(1, 2), (1, 3)]
+
+ expected_graph = nx.Graph()
+ expected_graph.add_edge(*intervals)
+
+ actual_g = interval_graph(intervals)
+
+ assert set(actual_g.nodes) == set(expected_graph.nodes)
+ assert edges_equal(expected_graph, actual_g)
+
+ def test_interval_graph_1(self):
+ intervals = [(1, 2), (2, 3), (3, 4), (1, 4)]
+
+ expected_graph = nx.Graph()
+ expected_graph.add_nodes_from(intervals)
+ e1 = ((1, 4), (1, 2))
+ e2 = ((1, 4), (2, 3))
+ e3 = ((1, 4), (3, 4))
+ e4 = ((3, 4), (2, 3))
+ e5 = ((1, 2), (2, 3))
+
+ expected_graph.add_edges_from([e1, e2, e3, e4, e5])
+
+ actual_g = interval_graph(intervals)
+
+ assert set(actual_g.nodes) == set(expected_graph.nodes)
+ assert edges_equal(expected_graph, actual_g)
+
+ def test_interval_graph_2(self):
+ intervals = [(1, 2), [3, 5], [6, 8], (9, 10)]
+
+ expected_graph = nx.Graph()
+ expected_graph.add_nodes_from([(1, 2), (3, 5), (6, 8), (9, 10)])
+
+ actual_g = interval_graph(intervals)
+
+ assert set(actual_g.nodes) == set(expected_graph.nodes)
+ assert edges_equal(expected_graph, actual_g)
+
+ def test_interval_graph_3(self):
+ intervals = [(1, 4), [3, 5], [2.5, 4]]
+
+ expected_graph = nx.Graph()
+ expected_graph.add_nodes_from([(1, 4), (3, 5), (2.5, 4)])
+ e1 = ((1, 4), (3, 5))
+ e2 = ((1, 4), (2.5, 4))
+ e3 = ((3, 5), (2.5, 4))
+
+ expected_graph.add_edges_from([e1, e2, e3])
+
+ actual_g = interval_graph(intervals)
+
+ assert set(actual_g.nodes) == set(expected_graph.nodes)
+ assert edges_equal(expected_graph, actual_g)
+
+ def test_interval_graph_4(self):
+ """test all possible overlaps"""
+ intervals = [
+ (0, 2),
+ (-2, -1),
+ (-2, 0),
+ (-2, 1),
+ (-2, 2),
+ (-2, 3),
+ (0, 1),
+ (0, 2),
+ (0, 3),
+ (1, 2),
+ (1, 3),
+ (2, 3),
+ (3, 4),
+ ]
+
+ expected_graph = nx.Graph()
+ expected_graph.add_nodes_from(intervals)
+ expected_nbrs = {
+ (-2, 0),
+ (-2, 1),
+ (-2, 2),
+ (-2, 3),
+ (0, 1),
+ (0, 2),
+ (0, 3),
+ (1, 2),
+ (1, 3),
+ (2, 3),
+ }
+ actual_g = nx.interval_graph(intervals)
+ actual_nbrs = nx.neighbors(actual_g, (0, 2))
+
+ assert set(actual_nbrs) == expected_nbrs
+
+ def test_interval_graph_5(self):
+ """this test is to see that an interval supports infinite number"""
+ intervals = {(-math.inf, 0), (-1, -1), (0.5, 0.5), (1, 1), (1, math.inf)}
+
+ expected_graph = nx.Graph()
+ expected_graph.add_nodes_from(intervals)
+ e1 = ((-math.inf, 0), (-1, -1))
+ e2 = ((1, 1), (1, math.inf))
+
+ expected_graph.add_edges_from([e1, e2])
+ actual_g = interval_graph(intervals)
+
+ assert set(actual_g.nodes) == set(expected_graph.nodes)
+ assert edges_equal(expected_graph, actual_g)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_joint_degree_seq.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_joint_degree_seq.py
new file mode 100644
index 00000000..1bc0df5c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_joint_degree_seq.py
@@ -0,0 +1,125 @@
+import time
+
+from networkx.algorithms.assortativity import degree_mixing_dict
+from networkx.generators import gnm_random_graph, powerlaw_cluster_graph
+from networkx.generators.joint_degree_seq import (
+ directed_joint_degree_graph,
+ is_valid_directed_joint_degree,
+ is_valid_joint_degree,
+ joint_degree_graph,
+)
+
+
+def test_is_valid_joint_degree():
+ """Tests for conditions that invalidate a joint degree dict"""
+
+ # valid joint degree that satisfies all five conditions
+ joint_degrees = {
+ 1: {4: 1},
+ 2: {2: 2, 3: 2, 4: 2},
+ 3: {2: 2, 4: 1},
+ 4: {1: 1, 2: 2, 3: 1},
+ }
+ assert is_valid_joint_degree(joint_degrees)
+
+ # test condition 1
+ # joint_degrees_1[1][4] not integer
+ joint_degrees_1 = {
+ 1: {4: 1.5},
+ 2: {2: 2, 3: 2, 4: 2},
+ 3: {2: 2, 4: 1},
+ 4: {1: 1.5, 2: 2, 3: 1},
+ }
+ assert not is_valid_joint_degree(joint_degrees_1)
+
+ # test condition 2
+ # degree_count[2] = sum(joint_degrees_2[2][j)/2, is not an int
+ # degree_count[4] = sum(joint_degrees_2[4][j)/4, is not an int
+ joint_degrees_2 = {
+ 1: {4: 1},
+ 2: {2: 2, 3: 2, 4: 3},
+ 3: {2: 2, 4: 1},
+ 4: {1: 1, 2: 3, 3: 1},
+ }
+ assert not is_valid_joint_degree(joint_degrees_2)
+
+ # test conditions 3 and 4
+ # joint_degrees_3[1][4]>degree_count[1]*degree_count[4]
+ joint_degrees_3 = {
+ 1: {4: 2},
+ 2: {2: 2, 3: 2, 4: 2},
+ 3: {2: 2, 4: 1},
+ 4: {1: 2, 2: 2, 3: 1},
+ }
+ assert not is_valid_joint_degree(joint_degrees_3)
+
+ # test condition 5
+ # joint_degrees_5[1][1] not even
+ joint_degrees_5 = {1: {1: 9}}
+ assert not is_valid_joint_degree(joint_degrees_5)
+
+
+def test_joint_degree_graph(ntimes=10):
+ for _ in range(ntimes):
+ seed = int(time.time())
+
+ n, m, p = 20, 10, 1
+ # generate random graph with model powerlaw_cluster and calculate
+ # its joint degree
+ g = powerlaw_cluster_graph(n, m, p, seed=seed)
+ joint_degrees_g = degree_mixing_dict(g, normalized=False)
+
+ # generate simple undirected graph with given joint degree
+ # joint_degrees_g
+ G = joint_degree_graph(joint_degrees_g)
+ joint_degrees_G = degree_mixing_dict(G, normalized=False)
+
+ # assert that the given joint degree is equal to the generated
+ # graph's joint degree
+ assert joint_degrees_g == joint_degrees_G
+
+
+def test_is_valid_directed_joint_degree():
+ in_degrees = [0, 1, 1, 2]
+ out_degrees = [1, 1, 1, 1]
+ nkk = {1: {1: 2, 2: 2}}
+ assert is_valid_directed_joint_degree(in_degrees, out_degrees, nkk)
+
+ # not realizable, values are not integers.
+ nkk = {1: {1: 1.5, 2: 2.5}}
+ assert not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk)
+
+ # not realizable, number of edges between 1-2 are insufficient.
+ nkk = {1: {1: 2, 2: 1}}
+ assert not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk)
+
+ # not realizable, in/out degree sequences have different number of nodes.
+ out_degrees = [1, 1, 1]
+ nkk = {1: {1: 2, 2: 2}}
+ assert not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk)
+
+ # not realizable, degree sequences have fewer than required nodes.
+ in_degrees = [0, 1, 2]
+ assert not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk)
+
+
+def test_directed_joint_degree_graph(n=15, m=100, ntimes=1000):
+ for _ in range(ntimes):
+ # generate gnm random graph and calculate its joint degree.
+ g = gnm_random_graph(n, m, None, directed=True)
+
+ # in-degree sequence of g as a list of integers.
+ in_degrees = list(dict(g.in_degree()).values())
+ # out-degree sequence of g as a list of integers.
+ out_degrees = list(dict(g.out_degree()).values())
+ nkk = degree_mixing_dict(g)
+
+ # generate simple directed graph with given degree sequence and joint
+ # degree matrix.
+ G = directed_joint_degree_graph(in_degrees, out_degrees, nkk)
+
+ # assert degree sequence correctness.
+ assert in_degrees == list(dict(G.in_degree()).values())
+ assert out_degrees == list(dict(G.out_degree()).values())
+ # assert joint degree matrix correctness.
+ assert nkk == degree_mixing_dict(G)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_lattice.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_lattice.py
new file mode 100644
index 00000000..5012324a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_lattice.py
@@ -0,0 +1,246 @@
+"""Unit tests for the :mod:`networkx.generators.lattice` module."""
+
+from itertools import product
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal
+
+
+class TestGrid2DGraph:
+ """Unit tests for :func:`networkx.generators.lattice.grid_2d_graph`"""
+
+ def test_number_of_vertices(self):
+ m, n = 5, 6
+ G = nx.grid_2d_graph(m, n)
+ assert len(G) == m * n
+
+ def test_degree_distribution(self):
+ m, n = 5, 6
+ G = nx.grid_2d_graph(m, n)
+ expected_histogram = [0, 0, 4, 2 * (m + n) - 8, (m - 2) * (n - 2)]
+ assert nx.degree_histogram(G) == expected_histogram
+
+ def test_directed(self):
+ m, n = 5, 6
+ G = nx.grid_2d_graph(m, n)
+ H = nx.grid_2d_graph(m, n, create_using=nx.DiGraph())
+ assert H.succ == G.adj
+ assert H.pred == G.adj
+
+ def test_multigraph(self):
+ m, n = 5, 6
+ G = nx.grid_2d_graph(m, n)
+ H = nx.grid_2d_graph(m, n, create_using=nx.MultiGraph())
+ assert list(H.edges()) == list(G.edges())
+
+ def test_periodic(self):
+ G = nx.grid_2d_graph(0, 0, periodic=True)
+ assert dict(G.degree()) == {}
+
+ for m, n, H in [
+ (2, 2, nx.cycle_graph(4)),
+ (1, 7, nx.cycle_graph(7)),
+ (7, 1, nx.cycle_graph(7)),
+ (2, 5, nx.circular_ladder_graph(5)),
+ (5, 2, nx.circular_ladder_graph(5)),
+ (2, 4, nx.cubical_graph()),
+ (4, 2, nx.cubical_graph()),
+ ]:
+ G = nx.grid_2d_graph(m, n, periodic=True)
+ assert nx.could_be_isomorphic(G, H)
+
+ def test_periodic_iterable(self):
+ m, n = 3, 7
+ for a, b in product([0, 1], [0, 1]):
+ G = nx.grid_2d_graph(m, n, periodic=(a, b))
+ assert G.number_of_nodes() == m * n
+ assert G.number_of_edges() == (m + a - 1) * n + (n + b - 1) * m
+
+ def test_periodic_directed(self):
+ G = nx.grid_2d_graph(4, 2, periodic=True)
+ H = nx.grid_2d_graph(4, 2, periodic=True, create_using=nx.DiGraph())
+ assert H.succ == G.adj
+ assert H.pred == G.adj
+
+ def test_periodic_multigraph(self):
+ G = nx.grid_2d_graph(4, 2, periodic=True)
+ H = nx.grid_2d_graph(4, 2, periodic=True, create_using=nx.MultiGraph())
+ assert list(G.edges()) == list(H.edges())
+
+ def test_exceptions(self):
+ pytest.raises(nx.NetworkXError, nx.grid_2d_graph, -3, 2)
+ pytest.raises(nx.NetworkXError, nx.grid_2d_graph, 3, -2)
+ pytest.raises(TypeError, nx.grid_2d_graph, 3.3, 2)
+ pytest.raises(TypeError, nx.grid_2d_graph, 3, 2.2)
+
+ def test_node_input(self):
+ G = nx.grid_2d_graph(4, 2, periodic=True)
+ H = nx.grid_2d_graph(range(4), range(2), periodic=True)
+ assert nx.is_isomorphic(H, G)
+ H = nx.grid_2d_graph("abcd", "ef", periodic=True)
+ assert nx.is_isomorphic(H, G)
+ G = nx.grid_2d_graph(5, 6)
+ H = nx.grid_2d_graph(range(5), range(6))
+ assert edges_equal(H, G)
+
+
+class TestGridGraph:
+ """Unit tests for :func:`networkx.generators.lattice.grid_graph`"""
+
+ def test_grid_graph(self):
+ """grid_graph([n,m]) is a connected simple graph with the
+ following properties:
+ number_of_nodes = n*m
+ degree_histogram = [0,0,4,2*(n+m)-8,(n-2)*(m-2)]
+ """
+ for n, m in [(3, 5), (5, 3), (4, 5), (5, 4)]:
+ dim = [n, m]
+ g = nx.grid_graph(dim)
+ assert len(g) == n * m
+ assert nx.degree_histogram(g) == [
+ 0,
+ 0,
+ 4,
+ 2 * (n + m) - 8,
+ (n - 2) * (m - 2),
+ ]
+
+ for n, m in [(1, 5), (5, 1)]:
+ dim = [n, m]
+ g = nx.grid_graph(dim)
+ assert len(g) == n * m
+ assert nx.is_isomorphic(g, nx.path_graph(5))
+
+ # mg = nx.grid_graph([n,m], create_using=MultiGraph())
+ # assert_equal(mg.edges(), g.edges())
+
+ def test_node_input(self):
+ G = nx.grid_graph([range(7, 9), range(3, 6)])
+ assert len(G) == 2 * 3
+ assert nx.is_isomorphic(G, nx.grid_graph([2, 3]))
+
+ def test_periodic_iterable(self):
+ m, n, k = 3, 7, 5
+ for a, b, c in product([0, 1], [0, 1], [0, 1]):
+ G = nx.grid_graph([m, n, k], periodic=(a, b, c))
+ num_e = (m + a - 1) * n * k + (n + b - 1) * m * k + (k + c - 1) * m * n
+ assert G.number_of_nodes() == m * n * k
+ assert G.number_of_edges() == num_e
+
+
+class TestHypercubeGraph:
+ """Unit tests for :func:`networkx.generators.lattice.hypercube_graph`"""
+
+ def test_special_cases(self):
+ for n, H in [
+ (0, nx.null_graph()),
+ (1, nx.path_graph(2)),
+ (2, nx.cycle_graph(4)),
+ (3, nx.cubical_graph()),
+ ]:
+ G = nx.hypercube_graph(n)
+ assert nx.could_be_isomorphic(G, H)
+
+ def test_degree_distribution(self):
+ for n in range(1, 10):
+ G = nx.hypercube_graph(n)
+ expected_histogram = [0] * n + [2**n]
+ assert nx.degree_histogram(G) == expected_histogram
+
+
+class TestTriangularLatticeGraph:
+ "Tests for :func:`networkx.generators.lattice.triangular_lattice_graph`"
+
+ def test_lattice_points(self):
+ """Tests that the graph is really a triangular lattice."""
+ for m, n in [(2, 3), (2, 2), (2, 1), (3, 3), (3, 2), (3, 4)]:
+ G = nx.triangular_lattice_graph(m, n)
+ N = (n + 1) // 2
+ assert len(G) == (m + 1) * (1 + N) - (n % 2) * ((m + 1) // 2)
+ for i, j in G.nodes():
+ nbrs = G[(i, j)]
+ if i < N:
+ assert (i + 1, j) in nbrs
+ if j < m:
+ assert (i, j + 1) in nbrs
+ if j < m and (i > 0 or j % 2) and (i < N or (j + 1) % 2):
+ assert (i + 1, j + 1) in nbrs or (i - 1, j + 1) in nbrs
+
+ def test_directed(self):
+ """Tests for creating a directed triangular lattice."""
+ G = nx.triangular_lattice_graph(3, 4, create_using=nx.Graph())
+ H = nx.triangular_lattice_graph(3, 4, create_using=nx.DiGraph())
+ assert H.is_directed()
+ for u, v in H.edges():
+ assert v[1] >= u[1]
+ if v[1] == u[1]:
+ assert v[0] > u[0]
+
+ def test_multigraph(self):
+ """Tests for creating a triangular lattice multigraph."""
+ G = nx.triangular_lattice_graph(3, 4, create_using=nx.Graph())
+ H = nx.triangular_lattice_graph(3, 4, create_using=nx.MultiGraph())
+ assert list(H.edges()) == list(G.edges())
+
+ def test_periodic(self):
+ G = nx.triangular_lattice_graph(4, 6, periodic=True)
+ assert len(G) == 12
+ assert G.size() == 36
+ # all degrees are 6
+ assert len([n for n, d in G.degree() if d != 6]) == 0
+ G = nx.triangular_lattice_graph(5, 7, periodic=True)
+ TLG = nx.triangular_lattice_graph
+ pytest.raises(nx.NetworkXError, TLG, 2, 4, periodic=True)
+ pytest.raises(nx.NetworkXError, TLG, 4, 4, periodic=True)
+ pytest.raises(nx.NetworkXError, TLG, 2, 6, periodic=True)
+
+
+class TestHexagonalLatticeGraph:
+ "Tests for :func:`networkx.generators.lattice.hexagonal_lattice_graph`"
+
+ def test_lattice_points(self):
+ """Tests that the graph is really a hexagonal lattice."""
+ for m, n in [(4, 5), (4, 4), (4, 3), (3, 2), (3, 3), (3, 5)]:
+ G = nx.hexagonal_lattice_graph(m, n)
+ assert len(G) == 2 * (m + 1) * (n + 1) - 2
+ C_6 = nx.cycle_graph(6)
+ hexagons = [
+ [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)],
+ [(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)],
+ [(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)],
+ [(2, 0), (2, 1), (2, 2), (3, 0), (3, 1), (3, 2)],
+ [(2, 2), (2, 3), (2, 4), (3, 2), (3, 3), (3, 4)],
+ ]
+ for hexagon in hexagons:
+ assert nx.is_isomorphic(G.subgraph(hexagon), C_6)
+
+ def test_directed(self):
+ """Tests for creating a directed hexagonal lattice."""
+ G = nx.hexagonal_lattice_graph(3, 5, create_using=nx.Graph())
+ H = nx.hexagonal_lattice_graph(3, 5, create_using=nx.DiGraph())
+ assert H.is_directed()
+ pos = nx.get_node_attributes(H, "pos")
+ for u, v in H.edges():
+ assert pos[v][1] >= pos[u][1]
+ if pos[v][1] == pos[u][1]:
+ assert pos[v][0] > pos[u][0]
+
+ def test_multigraph(self):
+ """Tests for creating a hexagonal lattice multigraph."""
+ G = nx.hexagonal_lattice_graph(3, 5, create_using=nx.Graph())
+ H = nx.hexagonal_lattice_graph(3, 5, create_using=nx.MultiGraph())
+ assert list(H.edges()) == list(G.edges())
+
+ def test_periodic(self):
+ G = nx.hexagonal_lattice_graph(4, 6, periodic=True)
+ assert len(G) == 48
+ assert G.size() == 72
+ # all degrees are 3
+ assert len([n for n, d in G.degree() if d != 3]) == 0
+ G = nx.hexagonal_lattice_graph(5, 8, periodic=True)
+ HLG = nx.hexagonal_lattice_graph
+ pytest.raises(nx.NetworkXError, HLG, 2, 7, periodic=True)
+ pytest.raises(nx.NetworkXError, HLG, 1, 4, periodic=True)
+ pytest.raises(nx.NetworkXError, HLG, 2, 1, periodic=True)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_line.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_line.py
new file mode 100644
index 00000000..7f5454eb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_line.py
@@ -0,0 +1,309 @@
+import pytest
+
+import networkx as nx
+from networkx.generators import line
+from networkx.utils import edges_equal
+
+
+class TestGeneratorLine:
+ def test_star(self):
+ G = nx.star_graph(5)
+ L = nx.line_graph(G)
+ assert nx.is_isomorphic(L, nx.complete_graph(5))
+
+ def test_path(self):
+ G = nx.path_graph(5)
+ L = nx.line_graph(G)
+ assert nx.is_isomorphic(L, nx.path_graph(4))
+
+ def test_cycle(self):
+ G = nx.cycle_graph(5)
+ L = nx.line_graph(G)
+ assert nx.is_isomorphic(L, G)
+
+ def test_digraph1(self):
+ G = nx.DiGraph([(0, 1), (0, 2), (0, 3)])
+ L = nx.line_graph(G)
+ # no edge graph, but with nodes
+ assert L.adj == {(0, 1): {}, (0, 2): {}, (0, 3): {}}
+
+ def test_multigraph1(self):
+ G = nx.MultiGraph([(0, 1), (0, 1), (1, 0), (0, 2), (2, 0), (0, 3)])
+ L = nx.line_graph(G)
+ # no edge graph, but with nodes
+ assert edges_equal(
+ L.edges(),
+ [
+ ((0, 3, 0), (0, 1, 0)),
+ ((0, 3, 0), (0, 2, 0)),
+ ((0, 3, 0), (0, 2, 1)),
+ ((0, 3, 0), (0, 1, 1)),
+ ((0, 3, 0), (0, 1, 2)),
+ ((0, 1, 0), (0, 1, 1)),
+ ((0, 1, 0), (0, 2, 0)),
+ ((0, 1, 0), (0, 1, 2)),
+ ((0, 1, 0), (0, 2, 1)),
+ ((0, 1, 1), (0, 1, 2)),
+ ((0, 1, 1), (0, 2, 0)),
+ ((0, 1, 1), (0, 2, 1)),
+ ((0, 1, 2), (0, 2, 0)),
+ ((0, 1, 2), (0, 2, 1)),
+ ((0, 2, 0), (0, 2, 1)),
+ ],
+ )
+
+ def test_multigraph2(self):
+ G = nx.MultiGraph([(1, 2), (2, 1)])
+ L = nx.line_graph(G)
+ assert edges_equal(L.edges(), [((1, 2, 0), (1, 2, 1))])
+
+ def test_multidigraph1(self):
+ G = nx.MultiDiGraph([(1, 2), (2, 1)])
+ L = nx.line_graph(G)
+ assert edges_equal(L.edges(), [((1, 2, 0), (2, 1, 0)), ((2, 1, 0), (1, 2, 0))])
+
+ def test_multidigraph2(self):
+ G = nx.MultiDiGraph([(0, 1), (0, 1), (0, 1), (1, 2)])
+ L = nx.line_graph(G)
+ assert edges_equal(
+ L.edges(),
+ [((0, 1, 0), (1, 2, 0)), ((0, 1, 1), (1, 2, 0)), ((0, 1, 2), (1, 2, 0))],
+ )
+
+ def test_digraph2(self):
+ G = nx.DiGraph([(0, 1), (1, 2), (2, 3)])
+ L = nx.line_graph(G)
+ assert edges_equal(L.edges(), [((0, 1), (1, 2)), ((1, 2), (2, 3))])
+
+ def test_create1(self):
+ G = nx.DiGraph([(0, 1), (1, 2), (2, 3)])
+ L = nx.line_graph(G, create_using=nx.Graph())
+ assert edges_equal(L.edges(), [((0, 1), (1, 2)), ((1, 2), (2, 3))])
+
+ def test_create2(self):
+ G = nx.Graph([(0, 1), (1, 2), (2, 3)])
+ L = nx.line_graph(G, create_using=nx.DiGraph())
+ assert edges_equal(L.edges(), [((0, 1), (1, 2)), ((1, 2), (2, 3))])
+
+
+class TestGeneratorInverseLine:
+ def test_example(self):
+ G = nx.Graph()
+ G_edges = [
+ [1, 2],
+ [1, 3],
+ [1, 4],
+ [1, 5],
+ [2, 3],
+ [2, 5],
+ [2, 6],
+ [2, 7],
+ [3, 4],
+ [3, 5],
+ [6, 7],
+ [6, 8],
+ [7, 8],
+ ]
+ G.add_edges_from(G_edges)
+ H = nx.inverse_line_graph(G)
+ solution = nx.Graph()
+ solution_edges = [
+ ("a", "b"),
+ ("a", "c"),
+ ("a", "d"),
+ ("a", "e"),
+ ("c", "d"),
+ ("e", "f"),
+ ("e", "g"),
+ ("f", "g"),
+ ]
+ solution.add_edges_from(solution_edges)
+ assert nx.is_isomorphic(H, solution)
+
+ def test_example_2(self):
+ G = nx.Graph()
+ G_edges = [[1, 2], [1, 3], [2, 3], [3, 4], [3, 5], [4, 5]]
+ G.add_edges_from(G_edges)
+ H = nx.inverse_line_graph(G)
+ solution = nx.Graph()
+ solution_edges = [("a", "c"), ("b", "c"), ("c", "d"), ("d", "e"), ("d", "f")]
+ solution.add_edges_from(solution_edges)
+ assert nx.is_isomorphic(H, solution)
+
+ def test_pair(self):
+ G = nx.path_graph(2)
+ H = nx.inverse_line_graph(G)
+ solution = nx.path_graph(3)
+ assert nx.is_isomorphic(H, solution)
+
+ def test_line(self):
+ G = nx.path_graph(5)
+ solution = nx.path_graph(6)
+ H = nx.inverse_line_graph(G)
+ assert nx.is_isomorphic(H, solution)
+
+ def test_triangle_graph(self):
+ G = nx.complete_graph(3)
+ H = nx.inverse_line_graph(G)
+ alternative_solution = nx.Graph()
+ alternative_solution.add_edges_from([[0, 1], [0, 2], [0, 3]])
+ # there are two alternative inverse line graphs for this case
+ # so long as we get one of them the test should pass
+ assert nx.is_isomorphic(H, G) or nx.is_isomorphic(H, alternative_solution)
+
+ def test_cycle(self):
+ G = nx.cycle_graph(5)
+ H = nx.inverse_line_graph(G)
+ assert nx.is_isomorphic(H, G)
+
+ def test_empty(self):
+ G = nx.Graph()
+ H = nx.inverse_line_graph(G)
+ assert nx.is_isomorphic(H, nx.complete_graph(1))
+
+ def test_K1(self):
+ G = nx.complete_graph(1)
+ H = nx.inverse_line_graph(G)
+ solution = nx.path_graph(2)
+ assert nx.is_isomorphic(H, solution)
+
+ def test_edgeless_graph(self):
+ G = nx.empty_graph(5)
+ with pytest.raises(nx.NetworkXError, match="edgeless graph"):
+ nx.inverse_line_graph(G)
+
+ def test_selfloops_error(self):
+ G = nx.cycle_graph(4)
+ G.add_edge(0, 0)
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G)
+
+ def test_non_line_graphs(self):
+ # Tests several known non-line graphs for impossibility
+ # Adapted from L.W.Beineke, "Characterizations of derived graphs"
+
+ # claw graph
+ claw = nx.star_graph(3)
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, claw)
+
+ # wheel graph with 6 nodes
+ wheel = nx.wheel_graph(6)
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, wheel)
+
+ # K5 with one edge remove
+ K5m = nx.complete_graph(5)
+ K5m.remove_edge(0, 1)
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, K5m)
+
+ # graph without any odd triangles (contains claw as induced subgraph)
+ G = nx.compose(nx.path_graph(2), nx.complete_bipartite_graph(2, 3))
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G)
+
+ ## Variations on a diamond graph
+
+ # Diamond + 2 edges (+ "roof")
+ G = nx.diamond_graph()
+ G.add_edges_from([(4, 0), (5, 3)])
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G)
+ G.add_edge(4, 5)
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G)
+
+ # Diamond + 2 connected edges
+ G = nx.diamond_graph()
+ G.add_edges_from([(4, 0), (4, 3)])
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G)
+
+ # Diamond + K3 + one edge (+ 2*K3)
+ G = nx.diamond_graph()
+ G.add_edges_from([(4, 0), (4, 1), (4, 2), (5, 3)])
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G)
+ G.add_edges_from([(5, 1), (5, 2)])
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G)
+
+ # 4 triangles
+ G = nx.diamond_graph()
+ G.add_edges_from([(4, 0), (4, 1), (5, 2), (5, 3)])
+ pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G)
+
+ def test_wrong_graph_type(self):
+ G = nx.DiGraph()
+ G_edges = [[0, 1], [0, 2], [0, 3]]
+ G.add_edges_from(G_edges)
+ pytest.raises(nx.NetworkXNotImplemented, nx.inverse_line_graph, G)
+
+ G = nx.MultiGraph()
+ G_edges = [[0, 1], [0, 2], [0, 3]]
+ G.add_edges_from(G_edges)
+ pytest.raises(nx.NetworkXNotImplemented, nx.inverse_line_graph, G)
+
+ def test_line_inverse_line_complete(self):
+ G = nx.complete_graph(10)
+ H = nx.line_graph(G)
+ J = nx.inverse_line_graph(H)
+ assert nx.is_isomorphic(G, J)
+
+ def test_line_inverse_line_path(self):
+ G = nx.path_graph(10)
+ H = nx.line_graph(G)
+ J = nx.inverse_line_graph(H)
+ assert nx.is_isomorphic(G, J)
+
+ def test_line_inverse_line_hypercube(self):
+ G = nx.hypercube_graph(5)
+ H = nx.line_graph(G)
+ J = nx.inverse_line_graph(H)
+ assert nx.is_isomorphic(G, J)
+
+ def test_line_inverse_line_cycle(self):
+ G = nx.cycle_graph(10)
+ H = nx.line_graph(G)
+ J = nx.inverse_line_graph(H)
+ assert nx.is_isomorphic(G, J)
+
+ def test_line_inverse_line_star(self):
+ G = nx.star_graph(20)
+ H = nx.line_graph(G)
+ J = nx.inverse_line_graph(H)
+ assert nx.is_isomorphic(G, J)
+
+ def test_line_inverse_line_multipartite(self):
+ G = nx.complete_multipartite_graph(3, 4, 5)
+ H = nx.line_graph(G)
+ J = nx.inverse_line_graph(H)
+ assert nx.is_isomorphic(G, J)
+
+ def test_line_inverse_line_dgm(self):
+ G = nx.dorogovtsev_goltsev_mendes_graph(4)
+ H = nx.line_graph(G)
+ J = nx.inverse_line_graph(H)
+ assert nx.is_isomorphic(G, J)
+
+ def test_line_different_node_types(self):
+ G = nx.path_graph([1, 2, 3, "a", "b", "c"])
+ H = nx.line_graph(G)
+ J = nx.inverse_line_graph(H)
+ assert nx.is_isomorphic(G, J)
+
+
+class TestGeneratorPrivateFunctions:
+ def test_triangles_error(self):
+ G = nx.diamond_graph()
+ pytest.raises(nx.NetworkXError, line._triangles, G, (4, 0))
+ pytest.raises(nx.NetworkXError, line._triangles, G, (0, 3))
+
+ def test_odd_triangles_error(self):
+ G = nx.diamond_graph()
+ pytest.raises(nx.NetworkXError, line._odd_triangle, G, (0, 1, 4))
+ pytest.raises(nx.NetworkXError, line._odd_triangle, G, (0, 1, 3))
+
+ def test_select_starting_cell_error(self):
+ G = nx.diamond_graph()
+ pytest.raises(nx.NetworkXError, line._select_starting_cell, G, (4, 0))
+ pytest.raises(nx.NetworkXError, line._select_starting_cell, G, (0, 3))
+
+ def test_diamond_graph(self):
+ G = nx.diamond_graph()
+ for edge in G.edges:
+ cell = line._select_starting_cell(G, starting_edge=edge)
+ # Starting cell should always be one of the two triangles
+ assert len(cell) == 3
+ assert all(v in G[u] for u in cell for v in cell if u != v)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_mycielski.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_mycielski.py
new file mode 100644
index 00000000..eb12b141
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_mycielski.py
@@ -0,0 +1,30 @@
+"""Unit tests for the :mod:`networkx.generators.mycielski` module."""
+
+import pytest
+
+import networkx as nx
+
+
+class TestMycielski:
+ def test_construction(self):
+ G = nx.path_graph(2)
+ M = nx.mycielskian(G)
+ assert nx.is_isomorphic(M, nx.cycle_graph(5))
+
+ def test_size(self):
+ G = nx.path_graph(2)
+ M = nx.mycielskian(G, 2)
+ assert len(M) == 11
+ assert M.size() == 20
+
+ def test_mycielski_graph_generator(self):
+ G = nx.mycielski_graph(1)
+ assert nx.is_isomorphic(G, nx.empty_graph(1))
+ G = nx.mycielski_graph(2)
+ assert nx.is_isomorphic(G, nx.path_graph(2))
+ G = nx.mycielski_graph(3)
+ assert nx.is_isomorphic(G, nx.cycle_graph(5))
+ G = nx.mycielski_graph(4)
+ assert nx.is_isomorphic(G, nx.mycielskian(nx.cycle_graph(5)))
+ with pytest.raises(nx.NetworkXError, match="must satisfy n >= 1"):
+ nx.mycielski_graph(0)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_nonisomorphic_trees.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_nonisomorphic_trees.py
new file mode 100644
index 00000000..c73d44ae
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_nonisomorphic_trees.py
@@ -0,0 +1,68 @@
+"""
+Unit tests for WROM algorithm generator in generators/nonisomorphic_trees.py
+"""
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal
+
+
+class TestGeneratorNonIsomorphicTrees:
+ def test_tree_structure(self):
+ # test for tree structure for nx.nonisomorphic_trees()
+ def f(x):
+ return list(nx.nonisomorphic_trees(x))
+
+ for i in f(6):
+ assert nx.is_tree(i)
+ for i in f(8):
+ assert nx.is_tree(i)
+
+ def test_nonisomorphism(self):
+ # test for nonisomorphism of trees for nx.nonisomorphic_trees()
+ def f(x):
+ return list(nx.nonisomorphic_trees(x))
+
+ trees = f(6)
+ for i in range(len(trees)):
+ for j in range(i + 1, len(trees)):
+ assert not nx.is_isomorphic(trees[i], trees[j])
+ trees = f(8)
+ for i in range(len(trees)):
+ for j in range(i + 1, len(trees)):
+ assert not nx.is_isomorphic(trees[i], trees[j])
+
+ def test_number_of_nonisomorphic_trees(self):
+ # http://oeis.org/A000055
+ assert nx.number_of_nonisomorphic_trees(2) == 1
+ assert nx.number_of_nonisomorphic_trees(3) == 1
+ assert nx.number_of_nonisomorphic_trees(4) == 2
+ assert nx.number_of_nonisomorphic_trees(5) == 3
+ assert nx.number_of_nonisomorphic_trees(6) == 6
+ assert nx.number_of_nonisomorphic_trees(7) == 11
+ assert nx.number_of_nonisomorphic_trees(8) == 23
+
+ def test_nonisomorphic_trees(self):
+ def f(x):
+ return list(nx.nonisomorphic_trees(x))
+
+ assert edges_equal(f(3)[0].edges(), [(0, 1), (0, 2)])
+ assert edges_equal(f(4)[0].edges(), [(0, 1), (0, 3), (1, 2)])
+ assert edges_equal(f(4)[1].edges(), [(0, 1), (0, 2), (0, 3)])
+
+ def test_nonisomorphic_trees_matrix(self):
+ trees_2 = [[[0, 1], [1, 0]]]
+ with pytest.deprecated_call():
+ assert list(nx.nonisomorphic_trees(2, create="matrix")) == trees_2
+
+ trees_3 = [[[0, 1, 1], [1, 0, 0], [1, 0, 0]]]
+ with pytest.deprecated_call():
+ assert list(nx.nonisomorphic_trees(3, create="matrix")) == trees_3
+
+ trees_4 = [
+ [[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]],
+ [[0, 1, 1, 1], [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0]],
+ ]
+ with pytest.deprecated_call():
+ assert list(nx.nonisomorphic_trees(4, create="matrix")) == trees_4
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_random_clustered.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_random_clustered.py
new file mode 100644
index 00000000..85066520
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_random_clustered.py
@@ -0,0 +1,33 @@
+import pytest
+
+import networkx as nx
+
+
+class TestRandomClusteredGraph:
+ def test_custom_joint_degree_sequence(self):
+ node = [1, 1, 1, 2, 1, 2, 0, 0]
+ tri = [0, 0, 0, 0, 0, 1, 1, 1]
+ joint_degree_sequence = zip(node, tri)
+ G = nx.random_clustered_graph(joint_degree_sequence)
+ assert G.number_of_nodes() == 8
+ assert G.number_of_edges() == 7
+
+ def test_tuple_joint_degree_sequence(self):
+ G = nx.random_clustered_graph([(1, 2), (2, 1), (1, 1), (1, 1), (1, 1), (2, 0)])
+ assert G.number_of_nodes() == 6
+ assert G.number_of_edges() == 10
+
+ def test_invalid_joint_degree_sequence_type(self):
+ with pytest.raises(nx.NetworkXError, match="Invalid degree sequence"):
+ nx.random_clustered_graph([[1, 1], [2, 1], [0, 1]])
+
+ def test_invalid_joint_degree_sequence_value(self):
+ with pytest.raises(nx.NetworkXError, match="Invalid degree sequence"):
+ nx.random_clustered_graph([[1, 1], [1, 2], [0, 1]])
+
+ def test_directed_graph_raises_error(self):
+ with pytest.raises(nx.NetworkXError, match="Directed Graph not supported"):
+ nx.random_clustered_graph(
+ [(1, 2), (2, 1), (1, 1), (1, 1), (1, 1), (2, 0)],
+ create_using=nx.DiGraph,
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_random_graphs.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_random_graphs.py
new file mode 100644
index 00000000..3262e542
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_random_graphs.py
@@ -0,0 +1,478 @@
+"""Unit tests for the :mod:`networkx.generators.random_graphs` module."""
+
+import pytest
+
+import networkx as nx
+
+_gnp_generators = [
+ nx.gnp_random_graph,
+ nx.fast_gnp_random_graph,
+ nx.binomial_graph,
+ nx.erdos_renyi_graph,
+]
+
+
+@pytest.mark.parametrize("generator", _gnp_generators)
+@pytest.mark.parametrize("directed", (True, False))
+def test_gnp_generators_negative_edge_probability(generator, directed):
+ """If the edge probability `p` is <=0, the resulting graph should have no edges."""
+ G = generator(10, -1.1, directed=directed)
+ assert len(G) == 10
+ assert G.number_of_edges() == 0
+ assert G.is_directed() == directed
+
+
+@pytest.mark.parametrize("generator", _gnp_generators)
+@pytest.mark.parametrize(
+ ("directed", "expected_num_edges"),
+ [(False, 45), (True, 90)],
+)
+def test_gnp_generators_greater_than_1_edge_probability(
+ generator, directed, expected_num_edges
+):
+ """If the edge probability `p` is >=1, the resulting graph should be complete."""
+ G = generator(10, 1.1, directed=directed)
+ assert len(G) == 10
+ assert G.number_of_edges() == expected_num_edges
+ assert G.is_directed() == directed
+
+
+@pytest.mark.parametrize("generator", _gnp_generators)
+@pytest.mark.parametrize("directed", (True, False))
+def test_gnp_generators_basic(generator, directed):
+ """If the edge probability `p` is >0 and <1, test only the basic properties."""
+ G = generator(10, 0.1, directed=directed)
+ assert len(G) == 10
+ assert G.is_directed() == directed
+
+
+@pytest.mark.parametrize("generator", _gnp_generators)
+def test_gnp_generators_for_p_close_to_1(generator):
+ """If the edge probability `p` is close to 1, the resulting graph should have all edges."""
+ runs = 100
+ edges = sum(
+ generator(10, 0.99999, directed=True).number_of_edges() for _ in range(runs)
+ )
+ assert abs(edges / float(runs) - 90) <= runs * 2.0 / 100
+
+
+@pytest.mark.parametrize("generator", _gnp_generators)
+@pytest.mark.parametrize("p", (0.2, 0.8))
+@pytest.mark.parametrize("directed", (True, False))
+def test_gnp_generators_edge_probability(generator, p, directed):
+ """Test that gnp generators generate edges according to the their probability `p`."""
+ runs = 5000
+ n = 5
+ edge_counts = [[0] * n for _ in range(n)]
+ for i in range(runs):
+ G = generator(n, p, directed=directed)
+ for v, w in G.edges:
+ edge_counts[v][w] += 1
+ if not directed:
+ edge_counts[w][v] += 1
+ for v in range(n):
+ for w in range(n):
+ if v == w:
+ # There should be no loops
+ assert edge_counts[v][w] == 0
+ else:
+ # Each edge should have been generated with probability close to p
+ assert abs(edge_counts[v][w] / float(runs) - p) <= 0.03
+
+
+@pytest.mark.parametrize(
+ "generator", [nx.gnp_random_graph, nx.binomial_graph, nx.erdos_renyi_graph]
+)
+@pytest.mark.parametrize(
+ ("seed", "directed", "expected_num_edges"),
+ [(42, False, 1219), (42, True, 2454), (314, False, 1247), (314, True, 2476)],
+)
+def test_gnp_random_graph_aliases(generator, seed, directed, expected_num_edges):
+ """Test that aliases give the same result with the same seed."""
+ G = generator(100, 0.25, seed=seed, directed=directed)
+ assert len(G) == 100
+ assert G.number_of_edges() == expected_num_edges
+ assert G.is_directed() == directed
+
+
+class TestGeneratorsRandom:
+ def test_random_graph(self):
+ seed = 42
+ G = nx.gnm_random_graph(100, 20, seed)
+ G = nx.gnm_random_graph(100, 20, seed, directed=True)
+ G = nx.dense_gnm_random_graph(100, 20, seed)
+
+ G = nx.barabasi_albert_graph(100, 1, seed)
+ G = nx.barabasi_albert_graph(100, 3, seed)
+ assert G.number_of_edges() == (97 * 3)
+
+ G = nx.barabasi_albert_graph(100, 3, seed, nx.complete_graph(5))
+ assert G.number_of_edges() == (10 + 95 * 3)
+
+ G = nx.extended_barabasi_albert_graph(100, 1, 0, 0, seed)
+ assert G.number_of_edges() == 99
+ G = nx.extended_barabasi_albert_graph(100, 3, 0, 0, seed)
+ assert G.number_of_edges() == 97 * 3
+ G = nx.extended_barabasi_albert_graph(100, 1, 0, 0.5, seed)
+ assert G.number_of_edges() == 99
+ G = nx.extended_barabasi_albert_graph(100, 2, 0.5, 0, seed)
+ assert G.number_of_edges() > 100 * 3
+ assert G.number_of_edges() < 100 * 4
+
+ G = nx.extended_barabasi_albert_graph(100, 2, 0.3, 0.3, seed)
+ assert G.number_of_edges() > 100 * 2
+ assert G.number_of_edges() < 100 * 4
+
+ G = nx.powerlaw_cluster_graph(100, 1, 1.0, seed)
+ G = nx.powerlaw_cluster_graph(100, 3, 0.0, seed)
+ assert G.number_of_edges() == (97 * 3)
+
+ G = nx.random_regular_graph(10, 20, seed)
+
+ pytest.raises(nx.NetworkXError, nx.random_regular_graph, 3, 21)
+ pytest.raises(nx.NetworkXError, nx.random_regular_graph, 33, 21)
+
+ constructor = [(10, 20, 0.8), (20, 40, 0.8)]
+ G = nx.random_shell_graph(constructor, seed)
+
+ def is_caterpillar(g):
+ """
+ A tree is a caterpillar iff all nodes of degree >=3 are surrounded
+ by at most two nodes of degree two or greater.
+ ref: http://mathworld.wolfram.com/CaterpillarGraph.html
+ """
+ deg_over_3 = [n for n in g if g.degree(n) >= 3]
+ for n in deg_over_3:
+ nbh_deg_over_2 = [nbh for nbh in g.neighbors(n) if g.degree(nbh) >= 2]
+ if not len(nbh_deg_over_2) <= 2:
+ return False
+ return True
+
+ def is_lobster(g):
+ """
+ A tree is a lobster if it has the property that the removal of leaf
+ nodes leaves a caterpillar graph (Gallian 2007)
+ ref: http://mathworld.wolfram.com/LobsterGraph.html
+ """
+ non_leafs = [n for n in g if g.degree(n) > 1]
+ return is_caterpillar(g.subgraph(non_leafs))
+
+ G = nx.random_lobster(10, 0.1, 0.5, seed)
+ assert max(G.degree(n) for n in G.nodes()) > 3
+ assert is_lobster(G)
+ pytest.raises(nx.NetworkXError, nx.random_lobster, 10, 0.1, 1, seed)
+ pytest.raises(nx.NetworkXError, nx.random_lobster, 10, 1, 1, seed)
+ pytest.raises(nx.NetworkXError, nx.random_lobster, 10, 1, 0.5, seed)
+
+ # docstring says this should be a caterpillar
+ G = nx.random_lobster(10, 0.1, 0.0, seed)
+ assert is_caterpillar(G)
+
+ # difficult to find seed that requires few tries
+ seq = nx.random_powerlaw_tree_sequence(10, 3, seed=14, tries=1)
+ G = nx.random_powerlaw_tree(10, 3, seed=14, tries=1)
+
+ def test_dual_barabasi_albert(self, m1=1, m2=4, p=0.5):
+ """
+ Tests that the dual BA random graph generated behaves consistently.
+
+ Tests the exceptions are raised as expected.
+
+ The graphs generation are repeated several times to prevent lucky shots
+
+ """
+ seeds = [42, 314, 2718]
+ initial_graph = nx.complete_graph(10)
+
+ for seed in seeds:
+ # This should be BA with m = m1
+ BA1 = nx.barabasi_albert_graph(100, m1, seed)
+ DBA1 = nx.dual_barabasi_albert_graph(100, m1, m2, 1, seed)
+ assert BA1.edges() == DBA1.edges()
+
+ # This should be BA with m = m2
+ BA2 = nx.barabasi_albert_graph(100, m2, seed)
+ DBA2 = nx.dual_barabasi_albert_graph(100, m1, m2, 0, seed)
+ assert BA2.edges() == DBA2.edges()
+
+ BA3 = nx.barabasi_albert_graph(100, m1, seed)
+ DBA3 = nx.dual_barabasi_albert_graph(100, m1, m1, p, seed)
+ # We can't compare edges here since randomness is "consumed" when drawing
+ # between m1 and m2
+ assert BA3.size() == DBA3.size()
+
+ DBA = nx.dual_barabasi_albert_graph(100, m1, m2, p, seed, initial_graph)
+ BA1 = nx.barabasi_albert_graph(100, m1, seed, initial_graph)
+ BA2 = nx.barabasi_albert_graph(100, m2, seed, initial_graph)
+ assert (
+ min(BA1.size(), BA2.size()) <= DBA.size() <= max(BA1.size(), BA2.size())
+ )
+
+ # Testing exceptions
+ dbag = nx.dual_barabasi_albert_graph
+ pytest.raises(nx.NetworkXError, dbag, m1, m1, m2, 0)
+ pytest.raises(nx.NetworkXError, dbag, m2, m1, m2, 0)
+ pytest.raises(nx.NetworkXError, dbag, 100, m1, m2, -0.5)
+ pytest.raises(nx.NetworkXError, dbag, 100, m1, m2, 1.5)
+ initial = nx.complete_graph(max(m1, m2) - 1)
+ pytest.raises(nx.NetworkXError, dbag, 100, m1, m2, p, initial_graph=initial)
+
+ def test_extended_barabasi_albert(self, m=2):
+ """
+ Tests that the extended BA random graph generated behaves consistently.
+
+ Tests the exceptions are raised as expected.
+
+ The graphs generation are repeated several times to prevent lucky-shots
+
+ """
+ seeds = [42, 314, 2718]
+
+ for seed in seeds:
+ BA_model = nx.barabasi_albert_graph(100, m, seed)
+ BA_model_edges = BA_model.number_of_edges()
+
+ # This behaves just like BA, the number of edges must be the same
+ G1 = nx.extended_barabasi_albert_graph(100, m, 0, 0, seed)
+ assert G1.size() == BA_model_edges
+
+ # More than twice more edges should have been added
+ G1 = nx.extended_barabasi_albert_graph(100, m, 0.8, 0, seed)
+ assert G1.size() > BA_model_edges * 2
+
+ # Only edge rewiring, so the number of edges less than original
+ G2 = nx.extended_barabasi_albert_graph(100, m, 0, 0.8, seed)
+ assert G2.size() == BA_model_edges
+
+ # Mixed scenario: less edges than G1 and more edges than G2
+ G3 = nx.extended_barabasi_albert_graph(100, m, 0.3, 0.3, seed)
+ assert G3.size() > G2.size()
+ assert G3.size() < G1.size()
+
+ # Testing exceptions
+ ebag = nx.extended_barabasi_albert_graph
+ pytest.raises(nx.NetworkXError, ebag, m, m, 0, 0)
+ pytest.raises(nx.NetworkXError, ebag, 1, 0.5, 0, 0)
+ pytest.raises(nx.NetworkXError, ebag, 100, 2, 0.5, 0.5)
+
+ def test_random_zero_regular_graph(self):
+ """Tests that a 0-regular graph has the correct number of nodes and
+ edges.
+
+ """
+ seed = 42
+ G = nx.random_regular_graph(0, 10, seed)
+ assert len(G) == 10
+ assert G.number_of_edges() == 0
+
+ def test_gnm(self):
+ G = nx.gnm_random_graph(10, 3)
+ assert len(G) == 10
+ assert G.number_of_edges() == 3
+
+ G = nx.gnm_random_graph(10, 3, seed=42)
+ assert len(G) == 10
+ assert G.number_of_edges() == 3
+
+ G = nx.gnm_random_graph(10, 100)
+ assert len(G) == 10
+ assert G.number_of_edges() == 45
+
+ G = nx.gnm_random_graph(10, 100, directed=True)
+ assert len(G) == 10
+ assert G.number_of_edges() == 90
+
+ G = nx.gnm_random_graph(10, -1.1)
+ assert len(G) == 10
+ assert G.number_of_edges() == 0
+
+ def test_watts_strogatz_big_k(self):
+ # Test to make sure than n <= k
+ pytest.raises(nx.NetworkXError, nx.watts_strogatz_graph, 10, 11, 0.25)
+ pytest.raises(nx.NetworkXError, nx.newman_watts_strogatz_graph, 10, 11, 0.25)
+
+ # could create an infinite loop, now doesn't
+ # infinite loop used to occur when a node has degree n-1 and needs to rewire
+ nx.watts_strogatz_graph(10, 9, 0.25, seed=0)
+ nx.newman_watts_strogatz_graph(10, 9, 0.5, seed=0)
+
+ # Test k==n scenario
+ nx.watts_strogatz_graph(10, 10, 0.25, seed=0)
+ nx.newman_watts_strogatz_graph(10, 10, 0.25, seed=0)
+
+ def test_random_kernel_graph(self):
+ def integral(u, w, z):
+ return c * (z - w)
+
+ def root(u, w, r):
+ return r / c + w
+
+ c = 1
+ graph = nx.random_kernel_graph(1000, integral, root)
+ graph = nx.random_kernel_graph(1000, integral, root, seed=42)
+ assert len(graph) == 1000
+
+
+@pytest.mark.parametrize(
+ ("k", "expected_num_nodes", "expected_num_edges"),
+ [
+ (2, 10, 10),
+ (4, 10, 20),
+ ],
+)
+def test_watts_strogatz(k, expected_num_nodes, expected_num_edges):
+ G = nx.watts_strogatz_graph(10, k, 0.25, seed=42)
+ assert len(G) == expected_num_nodes
+ assert G.number_of_edges() == expected_num_edges
+
+
+def test_newman_watts_strogatz_zero_probability():
+ G = nx.newman_watts_strogatz_graph(10, 2, 0.0, seed=42)
+ assert len(G) == 10
+ assert G.number_of_edges() == 10
+
+
+def test_newman_watts_strogatz_nonzero_probability():
+ G = nx.newman_watts_strogatz_graph(10, 4, 0.25, seed=42)
+ assert len(G) == 10
+ assert G.number_of_edges() >= 20
+
+
+def test_connected_watts_strogatz():
+ G = nx.connected_watts_strogatz_graph(10, 2, 0.1, tries=10, seed=42)
+ assert len(G) == 10
+ assert G.number_of_edges() == 10
+
+
+def test_connected_watts_strogatz_zero_tries():
+ with pytest.raises(nx.NetworkXError, match="Maximum number of tries exceeded"):
+ nx.connected_watts_strogatz_graph(10, 2, 0.1, tries=0)
+
+
+@pytest.mark.parametrize(
+ "generator, kwargs",
+ [
+ (nx.fast_gnp_random_graph, {"n": 20, "p": 0.2, "directed": False}),
+ (nx.fast_gnp_random_graph, {"n": 20, "p": 0.2, "directed": True}),
+ (nx.gnp_random_graph, {"n": 20, "p": 0.2, "directed": False}),
+ (nx.gnp_random_graph, {"n": 20, "p": 0.2, "directed": True}),
+ (nx.dense_gnm_random_graph, {"n": 30, "m": 4}),
+ (nx.gnm_random_graph, {"n": 30, "m": 4, "directed": False}),
+ (nx.gnm_random_graph, {"n": 30, "m": 4, "directed": True}),
+ (nx.newman_watts_strogatz_graph, {"n": 50, "k": 5, "p": 0.1}),
+ (nx.watts_strogatz_graph, {"n": 50, "k": 5, "p": 0.1}),
+ (nx.connected_watts_strogatz_graph, {"n": 50, "k": 5, "p": 0.1}),
+ (nx.random_regular_graph, {"d": 5, "n": 20}),
+ (nx.barabasi_albert_graph, {"n": 40, "m": 3}),
+ (nx.dual_barabasi_albert_graph, {"n": 40, "m1": 3, "m2": 2, "p": 0.1}),
+ (nx.extended_barabasi_albert_graph, {"n": 40, "m": 3, "p": 0.1, "q": 0.2}),
+ (nx.powerlaw_cluster_graph, {"n": 40, "m": 3, "p": 0.1}),
+ (nx.random_lobster, {"n": 40, "p1": 0.1, "p2": 0.2}),
+ (nx.random_shell_graph, {"constructor": [(10, 20, 0.8), (20, 40, 0.8)]}),
+ (nx.random_powerlaw_tree, {"n": 10, "seed": 14, "tries": 1}),
+ (
+ nx.random_kernel_graph,
+ {
+ "n": 10,
+ "kernel_integral": lambda u, w, z: z - w,
+ "kernel_root": lambda u, w, r: r + w,
+ },
+ ),
+ ],
+)
+@pytest.mark.parametrize("create_using_instance", [False, True])
+def test_create_using(generator, kwargs, create_using_instance):
+ class DummyGraph(nx.Graph):
+ pass
+
+ class DummyDiGraph(nx.DiGraph):
+ pass
+
+ create_using_type = DummyDiGraph if kwargs.get("directed") else DummyGraph
+ create_using = create_using_type() if create_using_instance else create_using_type
+ graph = generator(**kwargs, create_using=create_using)
+ assert isinstance(graph, create_using_type)
+
+
+@pytest.mark.parametrize("directed", [True, False])
+@pytest.mark.parametrize("fn", (nx.fast_gnp_random_graph, nx.gnp_random_graph))
+def test_gnp_fns_disallow_multigraph(fn, directed):
+ with pytest.raises(nx.NetworkXError, match="must not be a multi-graph"):
+ fn(20, 0.2, create_using=nx.MultiGraph)
+
+
+@pytest.mark.parametrize("fn", (nx.gnm_random_graph, nx.dense_gnm_random_graph))
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_gnm_fns_disallow_directed_and_multigraph(fn, graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ fn(10, 20, create_using=graphtype)
+
+
+@pytest.mark.parametrize(
+ "fn",
+ (
+ nx.newman_watts_strogatz_graph,
+ nx.watts_strogatz_graph,
+ nx.connected_watts_strogatz_graph,
+ ),
+)
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_watts_strogatz_disallow_directed_and_multigraph(fn, graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ fn(10, 2, 0.2, create_using=graphtype)
+
+
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_random_regular_graph_disallow_directed_and_multigraph(graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ nx.random_regular_graph(2, 10, create_using=graphtype)
+
+
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_barabasi_albert_disallow_directed_and_multigraph(graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ nx.barabasi_albert_graph(10, 3, create_using=graphtype)
+
+
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_dual_barabasi_albert_disallow_directed_and_multigraph(graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ nx.dual_barabasi_albert_graph(10, 2, 1, 0.4, create_using=graphtype)
+
+
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_extended_barabasi_albert_disallow_directed_and_multigraph(graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ nx.extended_barabasi_albert_graph(10, 2, 0.2, 0.3, create_using=graphtype)
+
+
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_powerlaw_cluster_disallow_directed_and_multigraph(graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ nx.powerlaw_cluster_graph(10, 5, 0.2, create_using=graphtype)
+
+
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_random_lobster_disallow_directed_and_multigraph(graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ nx.random_lobster(10, 0.1, 0.1, create_using=graphtype)
+
+
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_random_shell_disallow_directed_and_multigraph(graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ nx.random_shell_graph([(10, 20, 2), (10, 20, 5)], create_using=graphtype)
+
+
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_random_powerlaw_tree_disallow_directed_and_multigraph(graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ nx.random_powerlaw_tree(10, create_using=graphtype)
+
+
+@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+def test_random_kernel_disallow_directed_and_multigraph(graphtype):
+ with pytest.raises(nx.NetworkXError, match="must not be"):
+ nx.random_kernel_graph(
+ 10, lambda y, a, b: a + b, lambda u, w, r: r + w, create_using=graphtype
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_small.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_small.py
new file mode 100644
index 00000000..355d6d36
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_small.py
@@ -0,0 +1,208 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms.isomorphism.isomorph import graph_could_be_isomorphic
+
+is_isomorphic = graph_could_be_isomorphic
+
+"""Generators - Small
+=====================
+
+Some small graphs
+"""
+
+null = nx.null_graph()
+
+
+class TestGeneratorsSmall:
+ def test__LCF_graph(self):
+ # If n<=0, then return the null_graph
+ G = nx.LCF_graph(-10, [1, 2], 100)
+ assert is_isomorphic(G, null)
+ G = nx.LCF_graph(0, [1, 2], 3)
+ assert is_isomorphic(G, null)
+ G = nx.LCF_graph(0, [1, 2], 10)
+ assert is_isomorphic(G, null)
+
+ # Test that LCF(n,[],0) == cycle_graph(n)
+ for a, b, c in [(5, [], 0), (10, [], 0), (5, [], 1), (10, [], 10)]:
+ G = nx.LCF_graph(a, b, c)
+ assert is_isomorphic(G, nx.cycle_graph(a))
+
+ # Generate the utility graph K_{3,3}
+ G = nx.LCF_graph(6, [3, -3], 3)
+ utility_graph = nx.complete_bipartite_graph(3, 3)
+ assert is_isomorphic(G, utility_graph)
+
+ with pytest.raises(nx.NetworkXError, match="Directed Graph not supported"):
+ G = nx.LCF_graph(6, [3, -3], 3, create_using=nx.DiGraph)
+
+ def test_properties_named_small_graphs(self):
+ G = nx.bull_graph()
+ assert sorted(G) == list(range(5))
+ assert G.number_of_edges() == 5
+ assert sorted(d for n, d in G.degree()) == [1, 1, 2, 3, 3]
+ assert nx.diameter(G) == 3
+ assert nx.radius(G) == 2
+
+ G = nx.chvatal_graph()
+ assert sorted(G) == list(range(12))
+ assert G.number_of_edges() == 24
+ assert [d for n, d in G.degree()] == 12 * [4]
+ assert nx.diameter(G) == 2
+ assert nx.radius(G) == 2
+
+ G = nx.cubical_graph()
+ assert sorted(G) == list(range(8))
+ assert G.number_of_edges() == 12
+ assert [d for n, d in G.degree()] == 8 * [3]
+ assert nx.diameter(G) == 3
+ assert nx.radius(G) == 3
+
+ G = nx.desargues_graph()
+ assert sorted(G) == list(range(20))
+ assert G.number_of_edges() == 30
+ assert [d for n, d in G.degree()] == 20 * [3]
+
+ G = nx.diamond_graph()
+ assert sorted(G) == list(range(4))
+ assert sorted(d for n, d in G.degree()) == [2, 2, 3, 3]
+ assert nx.diameter(G) == 2
+ assert nx.radius(G) == 1
+
+ G = nx.dodecahedral_graph()
+ assert sorted(G) == list(range(20))
+ assert G.number_of_edges() == 30
+ assert [d for n, d in G.degree()] == 20 * [3]
+ assert nx.diameter(G) == 5
+ assert nx.radius(G) == 5
+
+ G = nx.frucht_graph()
+ assert sorted(G) == list(range(12))
+ assert G.number_of_edges() == 18
+ assert [d for n, d in G.degree()] == 12 * [3]
+ assert nx.diameter(G) == 4
+ assert nx.radius(G) == 3
+
+ G = nx.heawood_graph()
+ assert sorted(G) == list(range(14))
+ assert G.number_of_edges() == 21
+ assert [d for n, d in G.degree()] == 14 * [3]
+ assert nx.diameter(G) == 3
+ assert nx.radius(G) == 3
+
+ G = nx.hoffman_singleton_graph()
+ assert sorted(G) == list(range(50))
+ assert G.number_of_edges() == 175
+ assert [d for n, d in G.degree()] == 50 * [7]
+ assert nx.diameter(G) == 2
+ assert nx.radius(G) == 2
+
+ G = nx.house_graph()
+ assert sorted(G) == list(range(5))
+ assert G.number_of_edges() == 6
+ assert sorted(d for n, d in G.degree()) == [2, 2, 2, 3, 3]
+ assert nx.diameter(G) == 2
+ assert nx.radius(G) == 2
+
+ G = nx.house_x_graph()
+ assert sorted(G) == list(range(5))
+ assert G.number_of_edges() == 8
+ assert sorted(d for n, d in G.degree()) == [2, 3, 3, 4, 4]
+ assert nx.diameter(G) == 2
+ assert nx.radius(G) == 1
+
+ G = nx.icosahedral_graph()
+ assert sorted(G) == list(range(12))
+ assert G.number_of_edges() == 30
+ assert [d for n, d in G.degree()] == [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
+ assert nx.diameter(G) == 3
+ assert nx.radius(G) == 3
+
+ G = nx.krackhardt_kite_graph()
+ assert sorted(G) == list(range(10))
+ assert G.number_of_edges() == 18
+ assert sorted(d for n, d in G.degree()) == [1, 2, 3, 3, 3, 4, 4, 5, 5, 6]
+
+ G = nx.moebius_kantor_graph()
+ assert sorted(G) == list(range(16))
+ assert G.number_of_edges() == 24
+ assert [d for n, d in G.degree()] == 16 * [3]
+ assert nx.diameter(G) == 4
+
+ G = nx.octahedral_graph()
+ assert sorted(G) == list(range(6))
+ assert G.number_of_edges() == 12
+ assert [d for n, d in G.degree()] == 6 * [4]
+ assert nx.diameter(G) == 2
+ assert nx.radius(G) == 2
+
+ G = nx.pappus_graph()
+ assert sorted(G) == list(range(18))
+ assert G.number_of_edges() == 27
+ assert [d for n, d in G.degree()] == 18 * [3]
+ assert nx.diameter(G) == 4
+
+ G = nx.petersen_graph()
+ assert sorted(G) == list(range(10))
+ assert G.number_of_edges() == 15
+ assert [d for n, d in G.degree()] == 10 * [3]
+ assert nx.diameter(G) == 2
+ assert nx.radius(G) == 2
+
+ G = nx.sedgewick_maze_graph()
+ assert sorted(G) == list(range(8))
+ assert G.number_of_edges() == 10
+ assert sorted(d for n, d in G.degree()) == [1, 2, 2, 2, 3, 3, 3, 4]
+
+ G = nx.tetrahedral_graph()
+ assert sorted(G) == list(range(4))
+ assert G.number_of_edges() == 6
+ assert [d for n, d in G.degree()] == [3, 3, 3, 3]
+ assert nx.diameter(G) == 1
+ assert nx.radius(G) == 1
+
+ G = nx.truncated_cube_graph()
+ assert sorted(G) == list(range(24))
+ assert G.number_of_edges() == 36
+ assert [d for n, d in G.degree()] == 24 * [3]
+
+ G = nx.truncated_tetrahedron_graph()
+ assert sorted(G) == list(range(12))
+ assert G.number_of_edges() == 18
+ assert [d for n, d in G.degree()] == 12 * [3]
+
+ G = nx.tutte_graph()
+ assert sorted(G) == list(range(46))
+ assert G.number_of_edges() == 69
+ assert [d for n, d in G.degree()] == 46 * [3]
+
+ # Test create_using with directed or multigraphs on small graphs
+ pytest.raises(nx.NetworkXError, nx.tutte_graph, create_using=nx.DiGraph)
+ MG = nx.tutte_graph(create_using=nx.MultiGraph)
+ assert sorted(MG.edges()) == sorted(G.edges())
+
+
+@pytest.mark.parametrize(
+ "fn",
+ (
+ nx.bull_graph,
+ nx.chvatal_graph,
+ nx.cubical_graph,
+ nx.diamond_graph,
+ nx.house_graph,
+ nx.house_x_graph,
+ nx.icosahedral_graph,
+ nx.krackhardt_kite_graph,
+ nx.octahedral_graph,
+ nx.petersen_graph,
+ nx.truncated_cube_graph,
+ nx.tutte_graph,
+ ),
+)
+@pytest.mark.parametrize(
+ "create_using", (nx.DiGraph, nx.MultiDiGraph, nx.DiGraph([(0, 1)]))
+)
+def tests_raises_with_directed_create_using(fn, create_using):
+ with pytest.raises(nx.NetworkXError, match="Directed Graph not supported"):
+ fn(create_using=create_using)
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_spectral_graph_forge.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_spectral_graph_forge.py
new file mode 100644
index 00000000..b554bfd7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_spectral_graph_forge.py
@@ -0,0 +1,49 @@
+import pytest
+
+pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+
+from networkx import is_isomorphic
+from networkx.exception import NetworkXError
+from networkx.generators import karate_club_graph
+from networkx.generators.spectral_graph_forge import spectral_graph_forge
+from networkx.utils import nodes_equal
+
+
+def test_spectral_graph_forge():
+ G = karate_club_graph()
+
+ seed = 54321
+
+ # common cases, just checking node number preserving and difference
+ # between identity and modularity cases
+ H = spectral_graph_forge(G, 0.1, transformation="identity", seed=seed)
+ assert nodes_equal(G, H)
+
+ I = spectral_graph_forge(G, 0.1, transformation="identity", seed=seed)
+ assert nodes_equal(G, H)
+ assert is_isomorphic(I, H)
+
+ I = spectral_graph_forge(G, 0.1, transformation="modularity", seed=seed)
+ assert nodes_equal(G, I)
+
+ assert not is_isomorphic(I, H)
+
+ # with all the eigenvectors, output graph is identical to the input one
+ H = spectral_graph_forge(G, 1, transformation="modularity", seed=seed)
+ assert nodes_equal(G, H)
+ assert is_isomorphic(G, H)
+
+ # invalid alpha input value, it is silently truncated in [0,1]
+ H = spectral_graph_forge(G, -1, transformation="identity", seed=seed)
+ assert nodes_equal(G, H)
+
+ H = spectral_graph_forge(G, 10, transformation="identity", seed=seed)
+ assert nodes_equal(G, H)
+ assert is_isomorphic(G, H)
+
+ # invalid transformation mode, checking the error raising
+ pytest.raises(
+ NetworkXError, spectral_graph_forge, G, 0.1, transformation="unknown", seed=seed
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_stochastic.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_stochastic.py
new file mode 100644
index 00000000..0404d9d8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_stochastic.py
@@ -0,0 +1,72 @@
+"""Unit tests for the :mod:`networkx.generators.stochastic` module."""
+
+import pytest
+
+import networkx as nx
+
+
+class TestStochasticGraph:
+ """Unit tests for the :func:`~networkx.stochastic_graph` function."""
+
+ def test_default_weights(self):
+ G = nx.DiGraph()
+ G.add_edge(0, 1)
+ G.add_edge(0, 2)
+ S = nx.stochastic_graph(G)
+ assert nx.is_isomorphic(G, S)
+ assert sorted(S.edges(data=True)) == [
+ (0, 1, {"weight": 0.5}),
+ (0, 2, {"weight": 0.5}),
+ ]
+
+ def test_in_place(self):
+ """Tests for an in-place reweighting of the edges of the graph."""
+ G = nx.DiGraph()
+ G.add_edge(0, 1, weight=1)
+ G.add_edge(0, 2, weight=1)
+ nx.stochastic_graph(G, copy=False)
+ assert sorted(G.edges(data=True)) == [
+ (0, 1, {"weight": 0.5}),
+ (0, 2, {"weight": 0.5}),
+ ]
+
+ def test_arbitrary_weights(self):
+ G = nx.DiGraph()
+ G.add_edge(0, 1, weight=1)
+ G.add_edge(0, 2, weight=1)
+ S = nx.stochastic_graph(G)
+ assert sorted(S.edges(data=True)) == [
+ (0, 1, {"weight": 0.5}),
+ (0, 2, {"weight": 0.5}),
+ ]
+
+ def test_multidigraph(self):
+ G = nx.MultiDiGraph()
+ G.add_edges_from([(0, 1), (0, 1), (0, 2), (0, 2)])
+ S = nx.stochastic_graph(G)
+ d = {"weight": 0.25}
+ assert sorted(S.edges(data=True)) == [
+ (0, 1, d),
+ (0, 1, d),
+ (0, 2, d),
+ (0, 2, d),
+ ]
+
+ def test_zero_weights(self):
+ """Smoke test: ensure ZeroDivisionError is not raised."""
+ G = nx.DiGraph()
+ G.add_edge(0, 1, weight=0)
+ G.add_edge(0, 2, weight=0)
+ S = nx.stochastic_graph(G)
+ assert sorted(S.edges(data=True)) == [
+ (0, 1, {"weight": 0}),
+ (0, 2, {"weight": 0}),
+ ]
+
+ def test_graph_disallowed(self):
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.stochastic_graph(nx.Graph())
+
+ def test_multigraph_disallowed(self):
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.stochastic_graph(nx.MultiGraph())
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_sudoku.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_sudoku.py
new file mode 100644
index 00000000..7c3560aa
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_sudoku.py
@@ -0,0 +1,92 @@
+"""Unit tests for the :mod:`networkx.generators.sudoku_graph` module."""
+
+import pytest
+
+import networkx as nx
+
+
+def test_sudoku_negative():
+ """Raise an error when generating a Sudoku graph of order -1."""
+ pytest.raises(nx.NetworkXError, nx.sudoku_graph, n=-1)
+
+
+@pytest.mark.parametrize("n", [0, 1, 2, 3, 4])
+def test_sudoku_generator(n):
+ """Generate Sudoku graphs of various sizes and verify their properties."""
+ G = nx.sudoku_graph(n)
+ expected_nodes = n**4
+ expected_degree = (n - 1) * (3 * n + 1)
+ expected_edges = expected_nodes * expected_degree // 2
+ assert not G.is_directed()
+ assert not G.is_multigraph()
+ assert G.number_of_nodes() == expected_nodes
+ assert G.number_of_edges() == expected_edges
+ assert all(d == expected_degree for _, d in G.degree)
+
+ if n == 2:
+ assert sorted(G.neighbors(6)) == [2, 3, 4, 5, 7, 10, 14]
+ elif n == 3:
+ assert sorted(G.neighbors(42)) == [
+ 6,
+ 15,
+ 24,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ 40,
+ 41,
+ 43,
+ 44,
+ 51,
+ 52,
+ 53,
+ 60,
+ 69,
+ 78,
+ ]
+ elif n == 4:
+ assert sorted(G.neighbors(0)) == [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 32,
+ 33,
+ 34,
+ 35,
+ 48,
+ 49,
+ 50,
+ 51,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ ]
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_time_series.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_time_series.py
new file mode 100644
index 00000000..5d0cc90a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_time_series.py
@@ -0,0 +1,64 @@
+"""Unit tests for the :mod:`networkx.generators.time_series` module."""
+
+import itertools
+
+import networkx as nx
+
+
+def test_visibility_graph__empty_series__empty_graph():
+ null_graph = nx.visibility_graph([]) # move along nothing to see here
+ assert nx.is_empty(null_graph)
+
+
+def test_visibility_graph__single_value_ts__single_node_graph():
+ node_graph = nx.visibility_graph([10]) # So Lonely
+ assert node_graph.number_of_nodes() == 1
+ assert node_graph.number_of_edges() == 0
+
+
+def test_visibility_graph__two_values_ts__single_edge_graph():
+ edge_graph = nx.visibility_graph([10, 20]) # Two of Us
+ assert list(edge_graph.edges) == [(0, 1)]
+
+
+def test_visibility_graph__convex_series__complete_graph():
+ series = [i**2 for i in range(10)] # no obstructions
+ expected_series_length = len(series)
+
+ actual_graph = nx.visibility_graph(series)
+
+ assert actual_graph.number_of_nodes() == expected_series_length
+ assert actual_graph.number_of_edges() == 45
+ assert nx.is_isomorphic(actual_graph, nx.complete_graph(expected_series_length))
+
+
+def test_visibility_graph__concave_series__path_graph():
+ series = [-(i**2) for i in range(10)] # Slip Slidin' Away
+ expected_node_count = len(series)
+
+ actual_graph = nx.visibility_graph(series)
+
+ assert actual_graph.number_of_nodes() == expected_node_count
+ assert actual_graph.number_of_edges() == expected_node_count - 1
+ assert nx.is_isomorphic(actual_graph, nx.path_graph(expected_node_count))
+
+
+def test_visibility_graph__flat_series__path_graph():
+ series = [0] * 10 # living in 1D flatland
+ expected_node_count = len(series)
+
+ actual_graph = nx.visibility_graph(series)
+
+ assert actual_graph.number_of_nodes() == expected_node_count
+ assert actual_graph.number_of_edges() == expected_node_count - 1
+ assert nx.is_isomorphic(actual_graph, nx.path_graph(expected_node_count))
+
+
+def test_visibility_graph_cyclic_series():
+ series = list(itertools.islice(itertools.cycle((2, 1, 3)), 17)) # It's so bumpy!
+ expected_node_count = len(series)
+
+ actual_graph = nx.visibility_graph(series)
+
+ assert actual_graph.number_of_nodes() == expected_node_count
+ assert actual_graph.number_of_edges() == 25
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_trees.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_trees.py
new file mode 100644
index 00000000..7932436b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_trees.py
@@ -0,0 +1,195 @@
+import random
+
+import pytest
+
+import networkx as nx
+from networkx.utils import arbitrary_element, graphs_equal
+
+
+@pytest.mark.parametrize("prefix_tree_fn", (nx.prefix_tree, nx.prefix_tree_recursive))
+def test_basic_prefix_tree(prefix_tree_fn):
+ # This example is from the Wikipedia article "Trie"
+ # <https://en.wikipedia.org/wiki/Trie>.
+ strings = ["a", "to", "tea", "ted", "ten", "i", "in", "inn"]
+ T = prefix_tree_fn(strings)
+ root, NIL = 0, -1
+
+ def source_label(v):
+ return T.nodes[v]["source"]
+
+ # First, we check that the tree has the expected
+ # structure. Recall that each node that corresponds to one of
+ # the input strings has an edge to the NIL node.
+ #
+ # Consider the three children at level 1 in the trie.
+ a, i, t = sorted(T[root], key=source_label)
+ # Check the 'a' branch.
+ assert len(T[a]) == 1
+ nil = arbitrary_element(T[a])
+ assert len(T[nil]) == 0
+ # Check the 'i' branch.
+ assert len(T[i]) == 2
+ nil, in_ = sorted(T[i], key=source_label)
+ assert len(T[nil]) == 0
+ assert len(T[in_]) == 2
+ nil, inn = sorted(T[in_], key=source_label)
+ assert len(T[nil]) == 0
+ assert len(T[inn]) == 1
+ nil = arbitrary_element(T[inn])
+ assert len(T[nil]) == 0
+ # Check the 't' branch.
+ te, to = sorted(T[t], key=source_label)
+ assert len(T[to]) == 1
+ nil = arbitrary_element(T[to])
+ assert len(T[nil]) == 0
+ tea, ted, ten = sorted(T[te], key=source_label)
+ assert len(T[tea]) == 1
+ assert len(T[ted]) == 1
+ assert len(T[ten]) == 1
+ nil = arbitrary_element(T[tea])
+ assert len(T[nil]) == 0
+ nil = arbitrary_element(T[ted])
+ assert len(T[nil]) == 0
+ nil = arbitrary_element(T[ten])
+ assert len(T[nil]) == 0
+
+ # Next, we check that the "sources" of each of the nodes is the
+ # rightmost letter in the string corresponding to the path to
+ # that node.
+ assert source_label(root) is None
+ assert source_label(a) == "a"
+ assert source_label(i) == "i"
+ assert source_label(t) == "t"
+ assert source_label(in_) == "n"
+ assert source_label(inn) == "n"
+ assert source_label(to) == "o"
+ assert source_label(te) == "e"
+ assert source_label(tea) == "a"
+ assert source_label(ted) == "d"
+ assert source_label(ten) == "n"
+ assert source_label(NIL) == "NIL"
+
+
+@pytest.mark.parametrize(
+ "strings",
+ (
+ ["a", "to", "tea", "ted", "ten", "i", "in", "inn"],
+ ["ab", "abs", "ad"],
+ ["ab", "abs", "ad", ""],
+ ["distant", "disparaging", "distant", "diamond", "ruby"],
+ ),
+)
+def test_implementations_consistent(strings):
+ """Ensure results are consistent between prefix_tree implementations."""
+ assert graphs_equal(nx.prefix_tree(strings), nx.prefix_tree_recursive(strings))
+
+
+def test_random_labeled_rooted_tree():
+ for i in range(1, 10):
+ t1 = nx.random_labeled_rooted_tree(i, seed=42)
+ t2 = nx.random_labeled_rooted_tree(i, seed=42)
+ assert nx.utils.misc.graphs_equal(t1, t2)
+ assert nx.is_tree(t1)
+ assert "root" in t1.graph
+ assert "roots" not in t1.graph
+
+
+def test_random_labeled_tree_n_zero():
+ """Tests if n = 0 then the NetworkXPointlessConcept exception is raised."""
+ with pytest.raises(nx.NetworkXPointlessConcept):
+ T = nx.random_labeled_tree(0, seed=1234)
+ with pytest.raises(nx.NetworkXPointlessConcept):
+ T = nx.random_labeled_rooted_tree(0, seed=1234)
+
+
+def test_random_labeled_rooted_forest():
+ for i in range(1, 10):
+ t1 = nx.random_labeled_rooted_forest(i, seed=42)
+ t2 = nx.random_labeled_rooted_forest(i, seed=42)
+ assert nx.utils.misc.graphs_equal(t1, t2)
+ for c in nx.connected_components(t1):
+ assert nx.is_tree(t1.subgraph(c))
+ assert "root" not in t1.graph
+ assert "roots" in t1.graph
+
+
+def test_random_labeled_rooted_forest_n_zero():
+ """Tests generation of empty labeled forests."""
+ F = nx.random_labeled_rooted_forest(0, seed=1234)
+ assert len(F) == 0
+ assert len(F.graph["roots"]) == 0
+
+
+def test_random_unlabeled_rooted_tree():
+ for i in range(1, 10):
+ t1 = nx.random_unlabeled_rooted_tree(i, seed=42)
+ t2 = nx.random_unlabeled_rooted_tree(i, seed=42)
+ assert nx.utils.misc.graphs_equal(t1, t2)
+ assert nx.is_tree(t1)
+ assert "root" in t1.graph
+ assert "roots" not in t1.graph
+ t = nx.random_unlabeled_rooted_tree(15, number_of_trees=10, seed=43)
+ random.seed(43)
+ s = nx.random_unlabeled_rooted_tree(15, number_of_trees=10, seed=random)
+ for i in range(10):
+ assert nx.utils.misc.graphs_equal(t[i], s[i])
+ assert nx.is_tree(t[i])
+ assert "root" in t[i].graph
+ assert "roots" not in t[i].graph
+
+
+def test_random_unlabeled_tree_n_zero():
+ """Tests if n = 0 then the NetworkXPointlessConcept exception is raised."""
+ with pytest.raises(nx.NetworkXPointlessConcept):
+ T = nx.random_unlabeled_tree(0, seed=1234)
+ with pytest.raises(nx.NetworkXPointlessConcept):
+ T = nx.random_unlabeled_rooted_tree(0, seed=1234)
+
+
+def test_random_unlabeled_rooted_forest():
+ with pytest.raises(ValueError):
+ nx.random_unlabeled_rooted_forest(10, q=0, seed=42)
+ for i in range(1, 10):
+ for q in range(1, i + 1):
+ t1 = nx.random_unlabeled_rooted_forest(i, q=q, seed=42)
+ t2 = nx.random_unlabeled_rooted_forest(i, q=q, seed=42)
+ assert nx.utils.misc.graphs_equal(t1, t2)
+ for c in nx.connected_components(t1):
+ assert nx.is_tree(t1.subgraph(c))
+ assert len(c) <= q
+ assert "root" not in t1.graph
+ assert "roots" in t1.graph
+ t = nx.random_unlabeled_rooted_forest(15, number_of_forests=10, seed=43)
+ random.seed(43)
+ s = nx.random_unlabeled_rooted_forest(15, number_of_forests=10, seed=random)
+ for i in range(10):
+ assert nx.utils.misc.graphs_equal(t[i], s[i])
+ for c in nx.connected_components(t[i]):
+ assert nx.is_tree(t[i].subgraph(c))
+ assert "root" not in t[i].graph
+ assert "roots" in t[i].graph
+
+
+def test_random_unlabeled_forest_n_zero():
+ """Tests generation of empty unlabeled forests."""
+ F = nx.random_unlabeled_rooted_forest(0, seed=1234)
+ assert len(F) == 0
+ assert len(F.graph["roots"]) == 0
+
+
+def test_random_unlabeled_tree():
+ for i in range(1, 10):
+ t1 = nx.random_unlabeled_tree(i, seed=42)
+ t2 = nx.random_unlabeled_tree(i, seed=42)
+ assert nx.utils.misc.graphs_equal(t1, t2)
+ assert nx.is_tree(t1)
+ assert "root" not in t1.graph
+ assert "roots" not in t1.graph
+ t = nx.random_unlabeled_tree(10, number_of_trees=10, seed=43)
+ random.seed(43)
+ s = nx.random_unlabeled_tree(10, number_of_trees=10, seed=random)
+ for i in range(10):
+ assert nx.utils.misc.graphs_equal(t[i], s[i])
+ assert nx.is_tree(t[i])
+ assert "root" not in t[i].graph
+ assert "roots" not in t[i].graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_triads.py b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_triads.py
new file mode 100644
index 00000000..463844be
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/tests/test_triads.py
@@ -0,0 +1,15 @@
+"""Unit tests for the :mod:`networkx.generators.triads` module."""
+
+import pytest
+
+from networkx import triad_graph
+
+
+def test_triad_graph():
+ G = triad_graph("030T")
+ assert [tuple(e) for e in ("ab", "ac", "cb")] == sorted(G.edges())
+
+
+def test_invalid_name():
+ with pytest.raises(ValueError):
+ triad_graph("bogus")
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/time_series.py b/.venv/lib/python3.12/site-packages/networkx/generators/time_series.py
new file mode 100644
index 00000000..592d7734
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/time_series.py
@@ -0,0 +1,74 @@
+"""
+Time Series Graphs
+"""
+
+import itertools
+
+import networkx as nx
+
+__all__ = ["visibility_graph"]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def visibility_graph(series):
+ """
+ Return a Visibility Graph of an input Time Series.
+
+ A visibility graph converts a time series into a graph. The constructed graph
+ uses integer nodes to indicate which event in the series the node represents.
+ Edges are formed as follows: consider a bar plot of the series and view that
+ as a side view of a landscape with a node at the top of each bar. An edge
+ means that the nodes can be connected by a straight "line-of-sight" without
+ being obscured by any bars between the nodes.
+
+ The resulting graph inherits several properties of the series in its structure.
+ Thereby, periodic series convert into regular graphs, random series convert
+ into random graphs, and fractal series convert into scale-free networks [1]_.
+
+ Parameters
+ ----------
+ series : Sequence[Number]
+ A Time Series sequence (iterable and sliceable) of numeric values
+ representing times.
+
+ Returns
+ -------
+ NetworkX Graph
+ The Visibility Graph of the input series
+
+ Examples
+ --------
+ >>> series_list = [range(10), [2, 1, 3, 2, 1, 3, 2, 1, 3, 2, 1, 3]]
+ >>> for s in series_list:
+ ... g = nx.visibility_graph(s)
+ ... print(g)
+ Graph with 10 nodes and 9 edges
+ Graph with 12 nodes and 18 edges
+
+ References
+ ----------
+ .. [1] Lacasa, Lucas, Bartolo Luque, Fernando Ballesteros, Jordi Luque, and Juan Carlos Nuno.
+ "From time series to complex networks: The visibility graph." Proceedings of the
+ National Academy of Sciences 105, no. 13 (2008): 4972-4975.
+ https://www.pnas.org/doi/10.1073/pnas.0709247105
+ """
+
+ # Sequential values are always connected
+ G = nx.path_graph(len(series))
+ nx.set_node_attributes(G, dict(enumerate(series)), "value")
+
+ # Check all combinations of nodes n series
+ for (n1, t1), (n2, t2) in itertools.combinations(enumerate(series), 2):
+ # check if any value between obstructs line of sight
+ slope = (t2 - t1) / (n2 - n1)
+ offset = t2 - slope * n2
+
+ obstructed = any(
+ t >= slope * n + offset
+ for n, t in enumerate(series[n1 + 1 : n2], start=n1 + 1)
+ )
+
+ if not obstructed:
+ G.add_edge(n1, n2)
+
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/trees.py b/.venv/lib/python3.12/site-packages/networkx/generators/trees.py
new file mode 100644
index 00000000..30849a8d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/trees.py
@@ -0,0 +1,1071 @@
+"""Functions for generating trees.
+
+The functions sampling trees at random in this module come
+in two variants: labeled and unlabeled. The labeled variants
+sample from every possible tree with the given number of nodes
+uniformly at random. The unlabeled variants sample from every
+possible *isomorphism class* of trees with the given number
+of nodes uniformly at random.
+
+To understand the difference, consider the following example.
+There are two isomorphism classes of trees with four nodes.
+One is that of the path graph, the other is that of the
+star graph. The unlabeled variant will return a line graph or
+a star graph with probability 1/2.
+
+The labeled variant will return the line graph
+with probability 3/4 and the star graph with probability 1/4,
+because there are more labeled variants of the line graph
+than of the star graph. More precisely, the line graph has
+an automorphism group of order 2, whereas the star graph has
+an automorphism group of order 6, so the line graph has three
+times as many labeled variants as the star graph, and thus
+three more chances to be drawn.
+
+Additionally, some functions in this module can sample rooted
+trees and forests uniformly at random. A rooted tree is a tree
+with a designated root node. A rooted forest is a disjoint union
+of rooted trees.
+"""
+
+import warnings
+from collections import Counter, defaultdict
+from math import comb, factorial
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+__all__ = [
+ "prefix_tree",
+ "prefix_tree_recursive",
+ "random_labeled_tree",
+ "random_labeled_rooted_tree",
+ "random_labeled_rooted_forest",
+ "random_unlabeled_tree",
+ "random_unlabeled_rooted_tree",
+ "random_unlabeled_rooted_forest",
+]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def prefix_tree(paths):
+ """Creates a directed prefix tree from a list of paths.
+
+ Usually the paths are described as strings or lists of integers.
+
+ A "prefix tree" represents the prefix structure of the strings.
+ Each node represents a prefix of some string. The root represents
+ the empty prefix with children for the single letter prefixes which
+ in turn have children for each double letter prefix starting with
+ the single letter corresponding to the parent node, and so on.
+
+ More generally the prefixes do not need to be strings. A prefix refers
+ to the start of a sequence. The root has children for each one element
+ prefix and they have children for each two element prefix that starts
+ with the one element sequence of the parent, and so on.
+
+ Note that this implementation uses integer nodes with an attribute.
+ Each node has an attribute "source" whose value is the original element
+ of the path to which this node corresponds. For example, suppose `paths`
+ consists of one path: "can". Then the nodes `[1, 2, 3]` which represent
+ this path have "source" values "c", "a" and "n".
+
+ All the descendants of a node have a common prefix in the sequence/path
+ associated with that node. From the returned tree, the prefix for each
+ node can be constructed by traversing the tree up to the root and
+ accumulating the "source" values along the way.
+
+ The root node is always `0` and has "source" attribute `None`.
+ The root is the only node with in-degree zero.
+ The nil node is always `-1` and has "source" attribute `"NIL"`.
+ The nil node is the only node with out-degree zero.
+
+
+ Parameters
+ ----------
+ paths: iterable of paths
+ An iterable of paths which are themselves sequences.
+ Matching prefixes among these sequences are identified with
+ nodes of the prefix tree. One leaf of the tree is associated
+ with each path. (Identical paths are associated with the same
+ leaf of the tree.)
+
+
+ Returns
+ -------
+ tree: DiGraph
+ A directed graph representing an arborescence consisting of the
+ prefix tree generated by `paths`. Nodes are directed "downward",
+ from parent to child. A special "synthetic" root node is added
+ to be the parent of the first node in each path. A special
+ "synthetic" leaf node, the "nil" node `-1`, is added to be the child
+ of all nodes representing the last element in a path. (The
+ addition of this nil node technically makes this not an
+ arborescence but a directed acyclic graph; removing the nil node
+ makes it an arborescence.)
+
+
+ Notes
+ -----
+ The prefix tree is also known as a *trie*.
+
+
+ Examples
+ --------
+ Create a prefix tree from a list of strings with common prefixes::
+
+ >>> paths = ["ab", "abs", "ad"]
+ >>> T = nx.prefix_tree(paths)
+ >>> list(T.edges)
+ [(0, 1), (1, 2), (1, 4), (2, -1), (2, 3), (3, -1), (4, -1)]
+
+ The leaf nodes can be obtained as predecessors of the nil node::
+
+ >>> root, NIL = 0, -1
+ >>> list(T.predecessors(NIL))
+ [2, 3, 4]
+
+ To recover the original paths that generated the prefix tree,
+ traverse up the tree from the node `-1` to the node `0`::
+
+ >>> recovered = []
+ >>> for v in T.predecessors(NIL):
+ ... prefix = ""
+ ... while v != root:
+ ... prefix = str(T.nodes[v]["source"]) + prefix
+ ... v = next(T.predecessors(v)) # only one predecessor
+ ... recovered.append(prefix)
+ >>> sorted(recovered)
+ ['ab', 'abs', 'ad']
+ """
+
+ def get_children(parent, paths):
+ children = defaultdict(list)
+ # Populate dictionary with key(s) as the child/children of the root and
+ # value(s) as the remaining paths of the corresponding child/children
+ for path in paths:
+ # If path is empty, we add an edge to the NIL node.
+ if not path:
+ tree.add_edge(parent, NIL)
+ continue
+ child, *rest = path
+ # `child` may exist as the head of more than one path in `paths`.
+ children[child].append(rest)
+ return children
+
+ # Initialize the prefix tree with a root node and a nil node.
+ tree = nx.DiGraph()
+ root = 0
+ tree.add_node(root, source=None)
+ NIL = -1
+ tree.add_node(NIL, source="NIL")
+ children = get_children(root, paths)
+ stack = [(root, iter(children.items()))]
+ while stack:
+ parent, remaining_children = stack[-1]
+ try:
+ child, remaining_paths = next(remaining_children)
+ # Pop item off stack if there are no remaining children
+ except StopIteration:
+ stack.pop()
+ continue
+ # We relabel each child with an unused name.
+ new_name = len(tree) - 1
+ # The "source" node attribute stores the original node name.
+ tree.add_node(new_name, source=child)
+ tree.add_edge(parent, new_name)
+ children = get_children(new_name, remaining_paths)
+ stack.append((new_name, iter(children.items())))
+
+ return tree
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def prefix_tree_recursive(paths):
+ """Recursively creates a directed prefix tree from a list of paths.
+
+ The original recursive version of prefix_tree for comparison. It is
+ the same algorithm but the recursion is unrolled onto a stack.
+
+ Usually the paths are described as strings or lists of integers.
+
+ A "prefix tree" represents the prefix structure of the strings.
+ Each node represents a prefix of some string. The root represents
+ the empty prefix with children for the single letter prefixes which
+ in turn have children for each double letter prefix starting with
+ the single letter corresponding to the parent node, and so on.
+
+ More generally the prefixes do not need to be strings. A prefix refers
+ to the start of a sequence. The root has children for each one element
+ prefix and they have children for each two element prefix that starts
+ with the one element sequence of the parent, and so on.
+
+ Note that this implementation uses integer nodes with an attribute.
+ Each node has an attribute "source" whose value is the original element
+ of the path to which this node corresponds. For example, suppose `paths`
+ consists of one path: "can". Then the nodes `[1, 2, 3]` which represent
+ this path have "source" values "c", "a" and "n".
+
+ All the descendants of a node have a common prefix in the sequence/path
+ associated with that node. From the returned tree, ehe prefix for each
+ node can be constructed by traversing the tree up to the root and
+ accumulating the "source" values along the way.
+
+ The root node is always `0` and has "source" attribute `None`.
+ The root is the only node with in-degree zero.
+ The nil node is always `-1` and has "source" attribute `"NIL"`.
+ The nil node is the only node with out-degree zero.
+
+
+ Parameters
+ ----------
+ paths: iterable of paths
+ An iterable of paths which are themselves sequences.
+ Matching prefixes among these sequences are identified with
+ nodes of the prefix tree. One leaf of the tree is associated
+ with each path. (Identical paths are associated with the same
+ leaf of the tree.)
+
+
+ Returns
+ -------
+ tree: DiGraph
+ A directed graph representing an arborescence consisting of the
+ prefix tree generated by `paths`. Nodes are directed "downward",
+ from parent to child. A special "synthetic" root node is added
+ to be the parent of the first node in each path. A special
+ "synthetic" leaf node, the "nil" node `-1`, is added to be the child
+ of all nodes representing the last element in a path. (The
+ addition of this nil node technically makes this not an
+ arborescence but a directed acyclic graph; removing the nil node
+ makes it an arborescence.)
+
+
+ Notes
+ -----
+ The prefix tree is also known as a *trie*.
+
+
+ Examples
+ --------
+ Create a prefix tree from a list of strings with common prefixes::
+
+ >>> paths = ["ab", "abs", "ad"]
+ >>> T = nx.prefix_tree(paths)
+ >>> list(T.edges)
+ [(0, 1), (1, 2), (1, 4), (2, -1), (2, 3), (3, -1), (4, -1)]
+
+ The leaf nodes can be obtained as predecessors of the nil node.
+
+ >>> root, NIL = 0, -1
+ >>> list(T.predecessors(NIL))
+ [2, 3, 4]
+
+ To recover the original paths that generated the prefix tree,
+ traverse up the tree from the node `-1` to the node `0`::
+
+ >>> recovered = []
+ >>> for v in T.predecessors(NIL):
+ ... prefix = ""
+ ... while v != root:
+ ... prefix = str(T.nodes[v]["source"]) + prefix
+ ... v = next(T.predecessors(v)) # only one predecessor
+ ... recovered.append(prefix)
+ >>> sorted(recovered)
+ ['ab', 'abs', 'ad']
+ """
+
+ def _helper(paths, root, tree):
+ """Recursively create a trie from the given list of paths.
+
+ `paths` is a list of paths, each of which is itself a list of
+ nodes, relative to the given `root` (but not including it). This
+ list of paths will be interpreted as a tree-like structure, in
+ which two paths that share a prefix represent two branches of
+ the tree with the same initial segment.
+
+ `root` is the parent of the node at index 0 in each path.
+
+ `tree` is the "accumulator", the :class:`networkx.DiGraph`
+ representing the branching to which the new nodes and edges will
+ be added.
+
+ """
+ # For each path, remove the first node and make it a child of root.
+ # Any remaining paths then get processed recursively.
+ children = defaultdict(list)
+ for path in paths:
+ # If path is empty, we add an edge to the NIL node.
+ if not path:
+ tree.add_edge(root, NIL)
+ continue
+ child, *rest = path
+ # `child` may exist as the head of more than one path in `paths`.
+ children[child].append(rest)
+ # Add a node for each child, connect root, recurse to remaining paths
+ for child, remaining_paths in children.items():
+ # We relabel each child with an unused name.
+ new_name = len(tree) - 1
+ # The "source" node attribute stores the original node name.
+ tree.add_node(new_name, source=child)
+ tree.add_edge(root, new_name)
+ _helper(remaining_paths, new_name, tree)
+
+ # Initialize the prefix tree with a root node and a nil node.
+ tree = nx.DiGraph()
+ root = 0
+ tree.add_node(root, source=None)
+ NIL = -1
+ tree.add_node(NIL, source="NIL")
+ # Populate the tree.
+ _helper(paths, root, tree)
+ return tree
+
+
+@py_random_state("seed")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_labeled_tree(n, *, seed=None):
+ """Returns a labeled tree on `n` nodes chosen uniformly at random.
+
+ Generating uniformly distributed random Prüfer sequences and
+ converting them into the corresponding trees is a straightforward
+ method of generating uniformly distributed random labeled trees.
+ This function implements this method.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes, greater than zero.
+ seed : random_state
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`
+
+ Returns
+ -------
+ :class:`networkx.Graph`
+ A `networkx.Graph` with nodes in the set {0, …, *n* - 1}.
+
+ Raises
+ ------
+ NetworkXPointlessConcept
+ If `n` is zero (because the null graph is not a tree).
+
+ Examples
+ --------
+ >>> G = nx.random_labeled_tree(5, seed=42)
+ >>> nx.is_tree(G)
+ True
+ >>> G.edges
+ EdgeView([(0, 1), (0, 3), (0, 2), (2, 4)])
+
+ A tree with *arbitrarily directed* edges can be created by assigning
+ generated edges to a ``DiGraph``:
+
+ >>> DG = nx.DiGraph()
+ >>> DG.add_edges_from(G.edges)
+ >>> nx.is_tree(DG)
+ True
+ >>> DG.edges
+ OutEdgeView([(0, 1), (0, 3), (0, 2), (2, 4)])
+ """
+ # Cannot create a Prüfer sequence unless `n` is at least two.
+ if n == 0:
+ raise nx.NetworkXPointlessConcept("the null graph is not a tree")
+ if n == 1:
+ return nx.empty_graph(1)
+ return nx.from_prufer_sequence([seed.choice(range(n)) for i in range(n - 2)])
+
+
+@py_random_state("seed")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_labeled_rooted_tree(n, *, seed=None):
+ """Returns a labeled rooted tree with `n` nodes.
+
+ The returned tree is chosen uniformly at random from all labeled rooted trees.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ :class:`networkx.Graph`
+ A `networkx.Graph` with integer nodes 0 <= node <= `n` - 1.
+ The root of the tree is selected uniformly from the nodes.
+ The "root" graph attribute identifies the root of the tree.
+
+ Notes
+ -----
+ This function returns the result of :func:`random_labeled_tree`
+ with a randomly selected root.
+
+ Raises
+ ------
+ NetworkXPointlessConcept
+ If `n` is zero (because the null graph is not a tree).
+ """
+ t = random_labeled_tree(n, seed=seed)
+ t.graph["root"] = seed.randint(0, n - 1)
+ return t
+
+
+@py_random_state("seed")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_labeled_rooted_forest(n, *, seed=None):
+ """Returns a labeled rooted forest with `n` nodes.
+
+ The returned forest is chosen uniformly at random using a
+ generalization of Prüfer sequences [1]_ in the form described in [2]_.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ seed : random_state
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ :class:`networkx.Graph`
+ A `networkx.Graph` with integer nodes 0 <= node <= `n` - 1.
+ The "roots" graph attribute is a set of integers containing the roots.
+
+ References
+ ----------
+ .. [1] Knuth, Donald E. "Another Enumeration of Trees."
+ Canadian Journal of Mathematics, 20 (1968): 1077-1086.
+ https://doi.org/10.4153/CJM-1968-104-8
+ .. [2] Rubey, Martin. "Counting Spanning Trees". Diplomarbeit
+ zur Erlangung des akademischen Grades Magister der
+ Naturwissenschaften an der Formal- und Naturwissenschaftlichen
+ Fakultät der Universität Wien. Wien, May 2000.
+ """
+
+ # Select the number of roots by iterating over the cumulative count of trees
+ # with at most k roots
+ def _select_k(n, seed):
+ r = seed.randint(0, (n + 1) ** (n - 1) - 1)
+ cum_sum = 0
+ for k in range(1, n):
+ cum_sum += (factorial(n - 1) * n ** (n - k)) // (
+ factorial(k - 1) * factorial(n - k)
+ )
+ if r < cum_sum:
+ return k
+
+ return n
+
+ F = nx.empty_graph(n)
+ if n == 0:
+ F.graph["roots"] = {}
+ return F
+ # Select the number of roots k
+ k = _select_k(n, seed)
+ if k == n:
+ F.graph["roots"] = set(range(n))
+ return F # Nothing to do
+ # Select the roots
+ roots = seed.sample(range(n), k)
+ # Nonroots
+ p = set(range(n)).difference(roots)
+ # Coding sequence
+ N = [seed.randint(0, n - 1) for i in range(n - k - 1)]
+ # Multiset of elements in N also in p
+ degree = Counter([x for x in N if x in p])
+ # Iterator over the elements of p with degree zero
+ iterator = iter(x for x in p if degree[x] == 0)
+ u = last = next(iterator)
+ # This loop is identical to that for Prüfer sequences,
+ # except that we can draw nodes only from p
+ for v in N:
+ F.add_edge(u, v)
+ degree[v] -= 1
+ if v < last and degree[v] == 0:
+ u = v
+ else:
+ last = u = next(iterator)
+
+ F.add_edge(u, roots[0])
+ F.graph["roots"] = set(roots)
+ return F
+
+
+# The following functions support generation of unlabeled trees and forests.
+
+
+def _to_nx(edges, n_nodes, root=None, roots=None):
+ """
+ Converts the (edges, n_nodes) input to a :class:`networkx.Graph`.
+ The (edges, n_nodes) input is a list of even length, where each pair
+ of consecutive integers represents an edge, and an integer `n_nodes`.
+ Integers in the list are elements of `range(n_nodes)`.
+
+ Parameters
+ ----------
+ edges : list of ints
+ The flattened list of edges of the graph.
+ n_nodes : int
+ The number of nodes of the graph.
+ root: int (default=None)
+ If not None, the "root" attribute of the graph will be set to this value.
+ roots: collection of ints (default=None)
+ If not None, he "roots" attribute of the graph will be set to this value.
+
+ Returns
+ -------
+ :class:`networkx.Graph`
+ The graph with `n_nodes` nodes and edges given by `edges`.
+ """
+ G = nx.empty_graph(n_nodes)
+ G.add_edges_from(edges)
+ if root is not None:
+ G.graph["root"] = root
+ if roots is not None:
+ G.graph["roots"] = roots
+ return G
+
+
+def _num_rooted_trees(n, cache_trees):
+ """Returns the number of unlabeled rooted trees with `n` nodes.
+
+ See also https://oeis.org/A000081.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes
+ cache_trees : list of ints
+ The $i$-th element is the number of unlabeled rooted trees with $i$ nodes,
+ which is used as a cache (and is extended to length $n+1$ if needed)
+
+ Returns
+ -------
+ int
+ The number of unlabeled rooted trees with `n` nodes.
+ """
+ for n_i in range(len(cache_trees), n + 1):
+ cache_trees.append(
+ sum(
+ [
+ d * cache_trees[n_i - j * d] * cache_trees[d]
+ for d in range(1, n_i)
+ for j in range(1, (n_i - 1) // d + 1)
+ ]
+ )
+ // (n_i - 1)
+ )
+ return cache_trees[n]
+
+
+def _select_jd_trees(n, cache_trees, seed):
+ """Returns a pair $(j,d)$ with a specific probability
+
+ Given $n$, returns a pair of positive integers $(j,d)$ with the probability
+ specified in formula (5) of Chapter 29 of [1]_.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes
+ cache_trees : list of ints
+ Cache for :func:`_num_rooted_trees`.
+ seed : random_state
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ (int, int)
+ A pair of positive integers $(j,d)$ satisfying formula (5) of
+ Chapter 29 of [1]_.
+
+ References
+ ----------
+ .. [1] Nijenhuis, Albert, and Wilf, Herbert S.
+ "Combinatorial algorithms: for computers and calculators."
+ Academic Press, 1978.
+ https://doi.org/10.1016/C2013-0-11243-3
+ """
+ p = seed.randint(0, _num_rooted_trees(n, cache_trees) * (n - 1) - 1)
+ cumsum = 0
+ for d in range(n - 1, 0, -1):
+ for j in range(1, (n - 1) // d + 1):
+ cumsum += (
+ d
+ * _num_rooted_trees(n - j * d, cache_trees)
+ * _num_rooted_trees(d, cache_trees)
+ )
+ if p < cumsum:
+ return (j, d)
+
+
+def _random_unlabeled_rooted_tree(n, cache_trees, seed):
+ """Returns an unlabeled rooted tree with `n` nodes.
+
+ Returns an unlabeled rooted tree with `n` nodes chosen uniformly
+ at random using the "RANRUT" algorithm from [1]_.
+ The tree is returned in the form: (list_of_edges, number_of_nodes)
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes, greater than zero.
+ cache_trees : list ints
+ Cache for :func:`_num_rooted_trees`.
+ seed : random_state
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ (list_of_edges, number_of_nodes) : list, int
+ A random unlabeled rooted tree with `n` nodes as a 2-tuple
+ ``(list_of_edges, number_of_nodes)``.
+ The root is node 0.
+
+ References
+ ----------
+ .. [1] Nijenhuis, Albert, and Wilf, Herbert S.
+ "Combinatorial algorithms: for computers and calculators."
+ Academic Press, 1978.
+ https://doi.org/10.1016/C2013-0-11243-3
+ """
+ if n == 1:
+ edges, n_nodes = [], 1
+ return edges, n_nodes
+ if n == 2:
+ edges, n_nodes = [(0, 1)], 2
+ return edges, n_nodes
+
+ j, d = _select_jd_trees(n, cache_trees, seed)
+ t1, t1_nodes = _random_unlabeled_rooted_tree(n - j * d, cache_trees, seed)
+ t2, t2_nodes = _random_unlabeled_rooted_tree(d, cache_trees, seed)
+ t12 = [(0, t2_nodes * i + t1_nodes) for i in range(j)]
+ t1.extend(t12)
+ for _ in range(j):
+ t1.extend((n1 + t1_nodes, n2 + t1_nodes) for n1, n2 in t2)
+ t1_nodes += t2_nodes
+
+ return t1, t1_nodes
+
+
+@py_random_state("seed")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_unlabeled_rooted_tree(n, *, number_of_trees=None, seed=None):
+ """Returns a number of unlabeled rooted trees uniformly at random
+
+ Returns one or more (depending on `number_of_trees`)
+ unlabeled rooted trees with `n` nodes drawn uniformly
+ at random.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes
+ number_of_trees : int or None (default)
+ If not None, this number of trees is generated and returned.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ :class:`networkx.Graph` or list of :class:`networkx.Graph`
+ A single `networkx.Graph` (or a list thereof, if `number_of_trees`
+ is specified) with nodes in the set {0, …, *n* - 1}.
+ The "root" graph attribute identifies the root of the tree.
+
+ Notes
+ -----
+ The trees are generated using the "RANRUT" algorithm from [1]_.
+ The algorithm needs to compute some counting functions
+ that are relatively expensive: in case several trees are needed,
+ it is advisable to use the `number_of_trees` optional argument
+ to reuse the counting functions.
+
+ Raises
+ ------
+ NetworkXPointlessConcept
+ If `n` is zero (because the null graph is not a tree).
+
+ References
+ ----------
+ .. [1] Nijenhuis, Albert, and Wilf, Herbert S.
+ "Combinatorial algorithms: for computers and calculators."
+ Academic Press, 1978.
+ https://doi.org/10.1016/C2013-0-11243-3
+ """
+ if n == 0:
+ raise nx.NetworkXPointlessConcept("the null graph is not a tree")
+ cache_trees = [0, 1] # initial cache of number of rooted trees
+ if number_of_trees is None:
+ return _to_nx(*_random_unlabeled_rooted_tree(n, cache_trees, seed), root=0)
+ return [
+ _to_nx(*_random_unlabeled_rooted_tree(n, cache_trees, seed), root=0)
+ for i in range(number_of_trees)
+ ]
+
+
+def _num_rooted_forests(n, q, cache_forests):
+ """Returns the number of unlabeled rooted forests with `n` nodes, and with
+ no more than `q` nodes per tree. A recursive formula for this is (2) in
+ [1]_. This function is implemented using dynamic programming instead of
+ recursion.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ q : int
+ The maximum number of nodes for each tree of the forest.
+ cache_forests : list of ints
+ The $i$-th element is the number of unlabeled rooted forests with
+ $i$ nodes, and with no more than `q` nodes per tree; this is used
+ as a cache (and is extended to length `n` + 1 if needed).
+
+ Returns
+ -------
+ int
+ The number of unlabeled rooted forests with `n` nodes with no more than
+ `q` nodes per tree.
+
+ References
+ ----------
+ .. [1] Wilf, Herbert S. "The uniform selection of free trees."
+ Journal of Algorithms 2.2 (1981): 204-207.
+ https://doi.org/10.1016/0196-6774(81)90021-3
+ """
+ for n_i in range(len(cache_forests), n + 1):
+ q_i = min(n_i, q)
+ cache_forests.append(
+ sum(
+ [
+ d * cache_forests[n_i - j * d] * cache_forests[d - 1]
+ for d in range(1, q_i + 1)
+ for j in range(1, n_i // d + 1)
+ ]
+ )
+ // n_i
+ )
+
+ return cache_forests[n]
+
+
+def _select_jd_forests(n, q, cache_forests, seed):
+ """Given `n` and `q`, returns a pair of positive integers $(j,d)$
+ such that $j\\leq d$, with probability satisfying (F1) of [1]_.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ q : int
+ The maximum number of nodes for each tree of the forest.
+ cache_forests : list of ints
+ Cache for :func:`_num_rooted_forests`.
+ seed : random_state
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ (int, int)
+ A pair of positive integers $(j,d)$
+
+ References
+ ----------
+ .. [1] Wilf, Herbert S. "The uniform selection of free trees."
+ Journal of Algorithms 2.2 (1981): 204-207.
+ https://doi.org/10.1016/0196-6774(81)90021-3
+ """
+ p = seed.randint(0, _num_rooted_forests(n, q, cache_forests) * n - 1)
+ cumsum = 0
+ for d in range(q, 0, -1):
+ for j in range(1, n // d + 1):
+ cumsum += (
+ d
+ * _num_rooted_forests(n - j * d, q, cache_forests)
+ * _num_rooted_forests(d - 1, q, cache_forests)
+ )
+ if p < cumsum:
+ return (j, d)
+
+
+def _random_unlabeled_rooted_forest(n, q, cache_trees, cache_forests, seed):
+ """Returns an unlabeled rooted forest with `n` nodes, and with no more
+ than `q` nodes per tree, drawn uniformly at random. It is an implementation
+ of the algorithm "Forest" of [1]_.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ q : int
+ The maximum number of nodes per tree.
+ cache_trees :
+ Cache for :func:`_num_rooted_trees`.
+ cache_forests :
+ Cache for :func:`_num_rooted_forests`.
+ seed : random_state
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ (edges, n, r) : (list, int, list)
+ The forest (edges, n) and a list r of root nodes.
+
+ References
+ ----------
+ .. [1] Wilf, Herbert S. "The uniform selection of free trees."
+ Journal of Algorithms 2.2 (1981): 204-207.
+ https://doi.org/10.1016/0196-6774(81)90021-3
+ """
+ if n == 0:
+ return ([], 0, [])
+
+ j, d = _select_jd_forests(n, q, cache_forests, seed)
+ t1, t1_nodes, r1 = _random_unlabeled_rooted_forest(
+ n - j * d, q, cache_trees, cache_forests, seed
+ )
+ t2, t2_nodes = _random_unlabeled_rooted_tree(d, cache_trees, seed)
+ for _ in range(j):
+ r1.append(t1_nodes)
+ t1.extend((n1 + t1_nodes, n2 + t1_nodes) for n1, n2 in t2)
+ t1_nodes += t2_nodes
+ return t1, t1_nodes, r1
+
+
+@py_random_state("seed")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_unlabeled_rooted_forest(n, *, q=None, number_of_forests=None, seed=None):
+ """Returns a forest or list of forests selected at random.
+
+ Returns one or more (depending on `number_of_forests`)
+ unlabeled rooted forests with `n` nodes, and with no more than
+ `q` nodes per tree, drawn uniformly at random.
+ The "roots" graph attribute identifies the roots of the forest.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes
+ q : int or None (default)
+ The maximum number of nodes per tree.
+ number_of_forests : int or None (default)
+ If not None, this number of forests is generated and returned.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ :class:`networkx.Graph` or list of :class:`networkx.Graph`
+ A single `networkx.Graph` (or a list thereof, if `number_of_forests`
+ is specified) with nodes in the set {0, …, *n* - 1}.
+ The "roots" graph attribute is a set containing the roots
+ of the trees in the forest.
+
+ Notes
+ -----
+ This function implements the algorithm "Forest" of [1]_.
+ The algorithm needs to compute some counting functions
+ that are relatively expensive: in case several trees are needed,
+ it is advisable to use the `number_of_forests` optional argument
+ to reuse the counting functions.
+
+ Raises
+ ------
+ ValueError
+ If `n` is non-zero but `q` is zero.
+
+ References
+ ----------
+ .. [1] Wilf, Herbert S. "The uniform selection of free trees."
+ Journal of Algorithms 2.2 (1981): 204-207.
+ https://doi.org/10.1016/0196-6774(81)90021-3
+ """
+ if q is None:
+ q = n
+ if q == 0 and n != 0:
+ raise ValueError("q must be a positive integer if n is positive.")
+
+ cache_trees = [0, 1] # initial cache of number of rooted trees
+ cache_forests = [1] # initial cache of number of rooted forests
+
+ if number_of_forests is None:
+ g, nodes, rs = _random_unlabeled_rooted_forest(
+ n, q, cache_trees, cache_forests, seed
+ )
+ return _to_nx(g, nodes, roots=set(rs))
+
+ res = []
+ for i in range(number_of_forests):
+ g, nodes, rs = _random_unlabeled_rooted_forest(
+ n, q, cache_trees, cache_forests, seed
+ )
+ res.append(_to_nx(g, nodes, roots=set(rs)))
+ return res
+
+
+def _num_trees(n, cache_trees):
+ """Returns the number of unlabeled trees with `n` nodes.
+
+ See also https://oeis.org/A000055.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes.
+ cache_trees : list of ints
+ Cache for :func:`_num_rooted_trees`.
+
+ Returns
+ -------
+ int
+ The number of unlabeled trees with `n` nodes.
+ """
+ r = _num_rooted_trees(n, cache_trees) - sum(
+ [
+ _num_rooted_trees(j, cache_trees) * _num_rooted_trees(n - j, cache_trees)
+ for j in range(1, n // 2 + 1)
+ ]
+ )
+ if n % 2 == 0:
+ r += comb(_num_rooted_trees(n // 2, cache_trees) + 1, 2)
+ return r
+
+
+def _bicenter(n, cache, seed):
+ """Returns a bi-centroidal tree on `n` nodes drawn uniformly at random.
+
+ This function implements the algorithm Bicenter of [1]_.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes (must be even).
+ cache : list of ints.
+ Cache for :func:`_num_rooted_trees`.
+ seed : random_state
+ See :ref:`Randomness<randomness>`
+
+ Returns
+ -------
+ (edges, n)
+ The tree as a list of edges and number of nodes.
+
+ References
+ ----------
+ .. [1] Wilf, Herbert S. "The uniform selection of free trees."
+ Journal of Algorithms 2.2 (1981): 204-207.
+ https://doi.org/10.1016/0196-6774(81)90021-3
+ """
+ t, t_nodes = _random_unlabeled_rooted_tree(n // 2, cache, seed)
+ if seed.randint(0, _num_rooted_trees(n // 2, cache)) == 0:
+ t2, t2_nodes = t, t_nodes
+ else:
+ t2, t2_nodes = _random_unlabeled_rooted_tree(n // 2, cache, seed)
+ t.extend([(n1 + (n // 2), n2 + (n // 2)) for n1, n2 in t2])
+ t.append((0, n // 2))
+ return t, t_nodes + t2_nodes
+
+
+def _random_unlabeled_tree(n, cache_trees, cache_forests, seed):
+ """Returns a tree on `n` nodes drawn uniformly at random.
+ It implements the Wilf's algorithm "Free" of [1]_.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes, greater than zero.
+ cache_trees : list of ints
+ Cache for :func:`_num_rooted_trees`.
+ cache_forests : list of ints
+ Cache for :func:`_num_rooted_forests`.
+ seed : random_state
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`
+
+ Returns
+ -------
+ (edges, n)
+ The tree as a list of edges and number of nodes.
+
+ References
+ ----------
+ .. [1] Wilf, Herbert S. "The uniform selection of free trees."
+ Journal of Algorithms 2.2 (1981): 204-207.
+ https://doi.org/10.1016/0196-6774(81)90021-3
+ """
+ if n % 2 == 1:
+ p = 0
+ else:
+ p = comb(_num_rooted_trees(n // 2, cache_trees) + 1, 2)
+ if seed.randint(0, _num_trees(n, cache_trees) - 1) < p:
+ return _bicenter(n, cache_trees, seed)
+ else:
+ f, n_f, r = _random_unlabeled_rooted_forest(
+ n - 1, (n - 1) // 2, cache_trees, cache_forests, seed
+ )
+ for i in r:
+ f.append((i, n_f))
+ return f, n_f + 1
+
+
+@py_random_state("seed")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def random_unlabeled_tree(n, *, number_of_trees=None, seed=None):
+ """Returns a tree or list of trees chosen randomly.
+
+ Returns one or more (depending on `number_of_trees`)
+ unlabeled trees with `n` nodes drawn uniformly at random.
+
+ Parameters
+ ----------
+ n : int
+ The number of nodes
+ number_of_trees : int or None (default)
+ If not None, this number of trees is generated and returned.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ :class:`networkx.Graph` or list of :class:`networkx.Graph`
+ A single `networkx.Graph` (or a list thereof, if
+ `number_of_trees` is specified) with nodes in the set {0, …, *n* - 1}.
+
+ Raises
+ ------
+ NetworkXPointlessConcept
+ If `n` is zero (because the null graph is not a tree).
+
+ Notes
+ -----
+ This function generates an unlabeled tree uniformly at random using
+ Wilf's algorithm "Free" of [1]_. The algorithm needs to
+ compute some counting functions that are relatively expensive:
+ in case several trees are needed, it is advisable to use the
+ `number_of_trees` optional argument to reuse the counting
+ functions.
+
+ References
+ ----------
+ .. [1] Wilf, Herbert S. "The uniform selection of free trees."
+ Journal of Algorithms 2.2 (1981): 204-207.
+ https://doi.org/10.1016/0196-6774(81)90021-3
+ """
+ if n == 0:
+ raise nx.NetworkXPointlessConcept("the null graph is not a tree")
+
+ cache_trees = [0, 1] # initial cache of number of rooted trees
+ cache_forests = [1] # initial cache of number of rooted forests
+ if number_of_trees is None:
+ return _to_nx(*_random_unlabeled_tree(n, cache_trees, cache_forests, seed))
+ else:
+ return [
+ _to_nx(*_random_unlabeled_tree(n, cache_trees, cache_forests, seed))
+ for i in range(number_of_trees)
+ ]
diff --git a/.venv/lib/python3.12/site-packages/networkx/generators/triads.py b/.venv/lib/python3.12/site-packages/networkx/generators/triads.py
new file mode 100644
index 00000000..09b722dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/generators/triads.py
@@ -0,0 +1,94 @@
+# See https://github.com/networkx/networkx/pull/1474
+# Copyright 2011 Reya Group <http://www.reyagroup.com>
+# Copyright 2011 Alex Levenson <alex@isnotinvain.com>
+# Copyright 2011 Diederik van Liere <diederik.vanliere@rotman.utoronto.ca>
+"""Functions that generate the triad graphs, that is, the possible
+digraphs on three nodes.
+
+"""
+
+import networkx as nx
+from networkx.classes import DiGraph
+
+__all__ = ["triad_graph"]
+
+#: Dictionary mapping triad name to list of directed edges in the
+#: digraph representation of that triad (with nodes 'a', 'b', and 'c').
+TRIAD_EDGES = {
+ "003": [],
+ "012": ["ab"],
+ "102": ["ab", "ba"],
+ "021D": ["ba", "bc"],
+ "021U": ["ab", "cb"],
+ "021C": ["ab", "bc"],
+ "111D": ["ac", "ca", "bc"],
+ "111U": ["ac", "ca", "cb"],
+ "030T": ["ab", "cb", "ac"],
+ "030C": ["ba", "cb", "ac"],
+ "201": ["ab", "ba", "ac", "ca"],
+ "120D": ["bc", "ba", "ac", "ca"],
+ "120U": ["ab", "cb", "ac", "ca"],
+ "120C": ["ab", "bc", "ac", "ca"],
+ "210": ["ab", "bc", "cb", "ac", "ca"],
+ "300": ["ab", "ba", "bc", "cb", "ac", "ca"],
+}
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def triad_graph(triad_name):
+ """Returns the triad graph with the given name.
+
+ Each string in the following tuple is a valid triad name::
+
+ (
+ "003",
+ "012",
+ "102",
+ "021D",
+ "021U",
+ "021C",
+ "111D",
+ "111U",
+ "030T",
+ "030C",
+ "201",
+ "120D",
+ "120U",
+ "120C",
+ "210",
+ "300",
+ )
+
+ Each triad name corresponds to one of the possible valid digraph on
+ three nodes.
+
+ Parameters
+ ----------
+ triad_name : string
+ The name of a triad, as described above.
+
+ Returns
+ -------
+ :class:`~networkx.DiGraph`
+ The digraph on three nodes with the given name. The nodes of the
+ graph are the single-character strings 'a', 'b', and 'c'.
+
+ Raises
+ ------
+ ValueError
+ If `triad_name` is not the name of a triad.
+
+ See also
+ --------
+ triadic_census
+
+ """
+ if triad_name not in TRIAD_EDGES:
+ raise ValueError(
+ f'unknown triad name "{triad_name}"; use one of the triad names'
+ " in the TRIAD_NAMES constant"
+ )
+ G = DiGraph()
+ G.add_nodes_from("abc")
+ G.add_edges_from(TRIAD_EDGES[triad_name])
+ return G