about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/generators
diff options
context:
space:
mode:
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 differdiff --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