about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/swap.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/swap.py')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/swap.py406
1 files changed, 406 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/swap.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/swap.py
new file mode 100644
index 00000000..cb3cc1c0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/swap.py
@@ -0,0 +1,406 @@
+"""Swap edges in a graph."""
+
+import math
+
+import networkx as nx
+from networkx.utils import py_random_state
+
+__all__ = ["double_edge_swap", "connected_double_edge_swap", "directed_edge_swap"]
+
+
+@nx.utils.not_implemented_for("undirected")
+@py_random_state(3)
+@nx._dispatchable(mutates_input=True, returns_graph=True)
+def directed_edge_swap(G, *, nswap=1, max_tries=100, seed=None):
+    """Swap three edges in a directed graph while keeping the node degrees fixed.
+
+    A directed edge swap swaps three edges such that a -> b -> c -> d becomes
+    a -> c -> b -> d. This pattern of swapping allows all possible states with the
+    same in- and out-degree distribution in a directed graph to be reached.
+
+    If the swap would create parallel edges (e.g. if a -> c already existed in the
+    previous example), another attempt is made to find a suitable trio of edges.
+
+    Parameters
+    ----------
+    G : DiGraph
+       A directed graph
+
+    nswap : integer (optional, default=1)
+       Number of three-edge (directed) swaps to perform
+
+    max_tries : integer (optional, default=100)
+       Maximum number of attempts to swap edges
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    G : DiGraph
+       The graph after the edges are swapped.
+
+    Raises
+    ------
+    NetworkXError
+        If `G` is not directed, or
+        If nswap > max_tries, or
+        If there are fewer than 4 nodes or 3 edges in `G`.
+    NetworkXAlgorithmError
+        If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made
+
+    Notes
+    -----
+    Does not enforce any connectivity constraints.
+
+    The graph G is modified in place.
+
+    A later swap is allowed to undo a previous swap.
+
+    References
+    ----------
+    .. [1] Erdős, Péter L., et al. “A Simple Havel-Hakimi Type Algorithm to Realize
+           Graphical Degree Sequences of Directed Graphs.” ArXiv:0905.4913 [Math],
+           Jan. 2010. https://doi.org/10.48550/arXiv.0905.4913.
+           Published  2010 in Elec. J. Combinatorics (17(1)). R66.
+           http://www.combinatorics.org/Volume_17/PDF/v17i1r66.pdf
+    .. [2] “Combinatorics - Reaching All Possible Simple Directed Graphs with a given
+           Degree Sequence with 2-Edge Swaps.” Mathematics Stack Exchange,
+           https://math.stackexchange.com/questions/22272/. Accessed 30 May 2022.
+    """
+    if nswap > max_tries:
+        raise nx.NetworkXError("Number of swaps > number of tries allowed.")
+    if len(G) < 4:
+        raise nx.NetworkXError("DiGraph has fewer than four nodes.")
+    if len(G.edges) < 3:
+        raise nx.NetworkXError("DiGraph has fewer than 3 edges")
+
+    # Instead of choosing uniformly at random from a generated edge list,
+    # this algorithm chooses nonuniformly from the set of nodes with
+    # probability weighted by degree.
+    tries = 0
+    swapcount = 0
+    keys, degrees = zip(*G.degree())  # keys, degree
+    cdf = nx.utils.cumulative_distribution(degrees)  # cdf of degree
+    discrete_sequence = nx.utils.discrete_sequence
+
+    while swapcount < nswap:
+        # choose source node index from discrete distribution
+        start_index = discrete_sequence(1, cdistribution=cdf, seed=seed)[0]
+        start = keys[start_index]
+        tries += 1
+
+        if tries > max_tries:
+            msg = f"Maximum number of swap attempts ({tries}) exceeded before desired swaps achieved ({nswap})."
+            raise nx.NetworkXAlgorithmError(msg)
+
+        # If the given node doesn't have any out edges, then there isn't anything to swap
+        if G.out_degree(start) == 0:
+            continue
+        second = seed.choice(list(G.succ[start]))
+        if start == second:
+            continue
+
+        if G.out_degree(second) == 0:
+            continue
+        third = seed.choice(list(G.succ[second]))
+        if second == third:
+            continue
+
+        if G.out_degree(third) == 0:
+            continue
+        fourth = seed.choice(list(G.succ[third]))
+        if third == fourth:
+            continue
+
+        if (
+            third not in G.succ[start]
+            and fourth not in G.succ[second]
+            and second not in G.succ[third]
+        ):
+            # Swap nodes
+            G.add_edge(start, third)
+            G.add_edge(third, second)
+            G.add_edge(second, fourth)
+            G.remove_edge(start, second)
+            G.remove_edge(second, third)
+            G.remove_edge(third, fourth)
+            swapcount += 1
+
+    return G
+
+
+@py_random_state(3)
+@nx._dispatchable(mutates_input=True, returns_graph=True)
+def double_edge_swap(G, nswap=1, max_tries=100, seed=None):
+    """Swap two edges in the graph while keeping the node degrees fixed.
+
+    A double-edge swap removes two randomly chosen edges u-v and x-y
+    and creates the new edges u-x and v-y::
+
+     u--v            u  v
+            becomes  |  |
+     x--y            x  y
+
+    If either the edge u-x or v-y already exist no swap is performed
+    and another attempt is made to find a suitable edge pair.
+
+    Parameters
+    ----------
+    G : graph
+       An undirected graph
+
+    nswap : integer (optional, default=1)
+       Number of double-edge swaps to perform
+
+    max_tries : integer (optional)
+       Maximum number of attempts to swap edges
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    G : graph
+       The graph after double edge swaps.
+
+    Raises
+    ------
+    NetworkXError
+        If `G` is directed, or
+        If `nswap` > `max_tries`, or
+        If there are fewer than 4 nodes or 2 edges in `G`.
+    NetworkXAlgorithmError
+        If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made
+
+    Notes
+    -----
+    Does not enforce any connectivity constraints.
+
+    The graph G is modified in place.
+    """
+    if G.is_directed():
+        raise nx.NetworkXError(
+            "double_edge_swap() not defined for directed graphs. Use directed_edge_swap instead."
+        )
+    if nswap > max_tries:
+        raise nx.NetworkXError("Number of swaps > number of tries allowed.")
+    if len(G) < 4:
+        raise nx.NetworkXError("Graph has fewer than four nodes.")
+    if len(G.edges) < 2:
+        raise nx.NetworkXError("Graph has fewer than 2 edges")
+    # Instead of choosing uniformly at random from a generated edge list,
+    # this algorithm chooses nonuniformly from the set of nodes with
+    # probability weighted by degree.
+    n = 0
+    swapcount = 0
+    keys, degrees = zip(*G.degree())  # keys, degree
+    cdf = nx.utils.cumulative_distribution(degrees)  # cdf of degree
+    discrete_sequence = nx.utils.discrete_sequence
+    while swapcount < nswap:
+        #        if random.random() < 0.5: continue # trick to avoid periodicities?
+        # pick two random edges without creating edge list
+        # choose source node indices from discrete distribution
+        (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed)
+        if ui == xi:
+            continue  # same source, skip
+        u = keys[ui]  # convert index to label
+        x = keys[xi]
+        # choose target uniformly from neighbors
+        v = seed.choice(list(G[u]))
+        y = seed.choice(list(G[x]))
+        if v == y:
+            continue  # same target, skip
+        if (x not in G[u]) and (y not in G[v]):  # don't create parallel edges
+            G.add_edge(u, x)
+            G.add_edge(v, y)
+            G.remove_edge(u, v)
+            G.remove_edge(x, y)
+            swapcount += 1
+        if n >= max_tries:
+            e = (
+                f"Maximum number of swap attempts ({n}) exceeded "
+                f"before desired swaps achieved ({nswap})."
+            )
+            raise nx.NetworkXAlgorithmError(e)
+        n += 1
+    return G
+
+
+@py_random_state(3)
+@nx._dispatchable(mutates_input=True)
+def connected_double_edge_swap(G, nswap=1, _window_threshold=3, seed=None):
+    """Attempts the specified number of double-edge swaps in the graph `G`.
+
+    A double-edge swap removes two randomly chosen edges `(u, v)` and `(x,
+    y)` and creates the new edges `(u, x)` and `(v, y)`::
+
+     u--v            u  v
+            becomes  |  |
+     x--y            x  y
+
+    If either `(u, x)` or `(v, y)` already exist, then no swap is performed
+    so the actual number of swapped edges is always *at most* `nswap`.
+
+    Parameters
+    ----------
+    G : graph
+       An undirected graph
+
+    nswap : integer (optional, default=1)
+       Number of double-edge swaps to perform
+
+    _window_threshold : integer
+
+       The window size below which connectedness of the graph will be checked
+       after each swap.
+
+       The "window" in this function is a dynamically updated integer that
+       represents the number of swap attempts to make before checking if the
+       graph remains connected. It is an optimization used to decrease the
+       running time of the algorithm in exchange for increased complexity of
+       implementation.
+
+       If the window size is below this threshold, then the algorithm checks
+       after each swap if the graph remains connected by checking if there is a
+       path joining the two nodes whose edge was just removed. If the window
+       size is above this threshold, then the algorithm performs do all the
+       swaps in the window and only then check if the graph is still connected.
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    int
+       The number of successful swaps
+
+    Raises
+    ------
+
+    NetworkXError
+
+       If the input graph is not connected, or if the graph has fewer than four
+       nodes.
+
+    Notes
+    -----
+
+    The initial graph `G` must be connected, and the resulting graph is
+    connected. The graph `G` is modified in place.
+
+    References
+    ----------
+    .. [1] C. Gkantsidis and M. Mihail and E. Zegura,
+           The Markov chain simulation method for generating connected
+           power law random graphs, 2003.
+           http://citeseer.ist.psu.edu/gkantsidis03markov.html
+    """
+    if not nx.is_connected(G):
+        raise nx.NetworkXError("Graph not connected")
+    if len(G) < 4:
+        raise nx.NetworkXError("Graph has fewer than four nodes.")
+    n = 0
+    swapcount = 0
+    deg = G.degree()
+    # Label key for nodes
+    dk = [n for n, d in G.degree()]
+    cdf = nx.utils.cumulative_distribution([d for n, d in G.degree()])
+    discrete_sequence = nx.utils.discrete_sequence
+    window = 1
+    while n < nswap:
+        wcount = 0
+        swapped = []
+        # If the window is small, we just check each time whether the graph is
+        # connected by checking if the nodes that were just separated are still
+        # connected.
+        if window < _window_threshold:
+            # This Boolean keeps track of whether there was a failure or not.
+            fail = False
+            while wcount < window and n < nswap:
+                # Pick two random edges without creating the edge list. Choose
+                # source nodes from the discrete degree distribution.
+                (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed)
+                # If the source nodes are the same, skip this pair.
+                if ui == xi:
+                    continue
+                # Convert an index to a node label.
+                u = dk[ui]
+                x = dk[xi]
+                # Choose targets uniformly from neighbors.
+                v = seed.choice(list(G.neighbors(u)))
+                y = seed.choice(list(G.neighbors(x)))
+                # If the target nodes are the same, skip this pair.
+                if v == y:
+                    continue
+                if x not in G[u] and y not in G[v]:
+                    G.remove_edge(u, v)
+                    G.remove_edge(x, y)
+                    G.add_edge(u, x)
+                    G.add_edge(v, y)
+                    swapped.append((u, v, x, y))
+                    swapcount += 1
+                n += 1
+                # If G remains connected...
+                if nx.has_path(G, u, v):
+                    wcount += 1
+                # Otherwise, undo the changes.
+                else:
+                    G.add_edge(u, v)
+                    G.add_edge(x, y)
+                    G.remove_edge(u, x)
+                    G.remove_edge(v, y)
+                    swapcount -= 1
+                    fail = True
+            # If one of the swaps failed, reduce the window size.
+            if fail:
+                window = math.ceil(window / 2)
+            else:
+                window += 1
+        # If the window is large, then there is a good chance that a bunch of
+        # swaps will work. It's quicker to do all those swaps first and then
+        # check if the graph remains connected.
+        else:
+            while wcount < window and n < nswap:
+                # Pick two random edges without creating the edge list. Choose
+                # source nodes from the discrete degree distribution.
+                (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed)
+                # If the source nodes are the same, skip this pair.
+                if ui == xi:
+                    continue
+                # Convert an index to a node label.
+                u = dk[ui]
+                x = dk[xi]
+                # Choose targets uniformly from neighbors.
+                v = seed.choice(list(G.neighbors(u)))
+                y = seed.choice(list(G.neighbors(x)))
+                # If the target nodes are the same, skip this pair.
+                if v == y:
+                    continue
+                if x not in G[u] and y not in G[v]:
+                    G.remove_edge(u, v)
+                    G.remove_edge(x, y)
+                    G.add_edge(u, x)
+                    G.add_edge(v, y)
+                    swapped.append((u, v, x, y))
+                    swapcount += 1
+                n += 1
+                wcount += 1
+            # If the graph remains connected, increase the window size.
+            if nx.is_connected(G):
+                window += 1
+            # Otherwise, undo the changes from the previous window and decrease
+            # the window size.
+            else:
+                while swapped:
+                    (u, v, x, y) = swapped.pop()
+                    G.add_edge(u, v)
+                    G.add_edge(x, y)
+                    G.remove_edge(u, x)
+                    G.remove_edge(v, y)
+                    swapcount -= 1
+                window = math.ceil(window / 2)
+    return swapcount