swap.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. """Swap edges in a graph.
  2. """
  3. import math
  4. import networkx as nx
  5. from networkx.utils import py_random_state
  6. __all__ = ["double_edge_swap", "connected_double_edge_swap", "directed_edge_swap"]
  7. @py_random_state(3)
  8. @nx.utils.not_implemented_for("undirected")
  9. def directed_edge_swap(G, *, nswap=1, max_tries=100, seed=None):
  10. """Swap three edges in a directed graph while keeping the node degrees fixed.
  11. A directed edge swap swaps three edges such that a -> b -> c -> d becomes
  12. a -> c -> b -> d. This pattern of swapping allows all possible states with the
  13. same in- and out-degree distribution in a directed graph to be reached.
  14. If the swap would create parallel edges (e.g. if a -> c already existed in the
  15. previous example), another attempt is made to find a suitable trio of edges.
  16. Parameters
  17. ----------
  18. G : DiGraph
  19. A directed graph
  20. nswap : integer (optional, default=1)
  21. Number of three-edge (directed) swaps to perform
  22. max_tries : integer (optional, default=100)
  23. Maximum number of attempts to swap edges
  24. seed : integer, random_state, or None (default)
  25. Indicator of random number generation state.
  26. See :ref:`Randomness<randomness>`.
  27. Returns
  28. -------
  29. G : DiGraph
  30. The graph after the edges are swapped.
  31. Raises
  32. ------
  33. NetworkXError
  34. If `G` is not directed, or
  35. If nswap > max_tries, or
  36. If there are fewer than 4 nodes or 3 edges in `G`.
  37. NetworkXAlgorithmError
  38. If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made
  39. Notes
  40. -----
  41. Does not enforce any connectivity constraints.
  42. The graph G is modified in place.
  43. References
  44. ----------
  45. .. [1] Erdős, Péter L., et al. “A Simple Havel-Hakimi Type Algorithm to Realize
  46. Graphical Degree Sequences of Directed Graphs.” ArXiv:0905.4913 [Math],
  47. Jan. 2010. https://doi.org/10.48550/arXiv.0905.4913.
  48. Published 2010 in Elec. J. Combinatorics (17(1)). R66.
  49. http://www.combinatorics.org/Volume_17/PDF/v17i1r66.pdf
  50. .. [2] “Combinatorics - Reaching All Possible Simple Directed Graphs with a given
  51. Degree Sequence with 2-Edge Swaps.” Mathematics Stack Exchange,
  52. https://math.stackexchange.com/questions/22272/. Accessed 30 May 2022.
  53. """
  54. if nswap > max_tries:
  55. raise nx.NetworkXError("Number of swaps > number of tries allowed.")
  56. if len(G) < 4:
  57. raise nx.NetworkXError("DiGraph has fewer than four nodes.")
  58. if len(G.edges) < 3:
  59. raise nx.NetworkXError("DiGraph has fewer than 3 edges")
  60. # Instead of choosing uniformly at random from a generated edge list,
  61. # this algorithm chooses nonuniformly from the set of nodes with
  62. # probability weighted by degree.
  63. tries = 0
  64. swapcount = 0
  65. keys, degrees = zip(*G.degree()) # keys, degree
  66. cdf = nx.utils.cumulative_distribution(degrees) # cdf of degree
  67. discrete_sequence = nx.utils.discrete_sequence
  68. while swapcount < nswap:
  69. # choose source node index from discrete distribution
  70. start_index = discrete_sequence(1, cdistribution=cdf, seed=seed)[0]
  71. start = keys[start_index]
  72. tries += 1
  73. if tries > max_tries:
  74. msg = f"Maximum number of swap attempts ({tries}) exceeded before desired swaps achieved ({nswap})."
  75. raise nx.NetworkXAlgorithmError(msg)
  76. # If the given node doesn't have any out edges, then there isn't anything to swap
  77. if G.out_degree(start) == 0:
  78. continue
  79. second = seed.choice(list(G.succ[start]))
  80. if start == second:
  81. continue
  82. if G.out_degree(second) == 0:
  83. continue
  84. third = seed.choice(list(G.succ[second]))
  85. if second == third:
  86. continue
  87. if G.out_degree(third) == 0:
  88. continue
  89. fourth = seed.choice(list(G.succ[third]))
  90. if third == fourth:
  91. continue
  92. if (
  93. third not in G.succ[start]
  94. and fourth not in G.succ[second]
  95. and second not in G.succ[third]
  96. ):
  97. # Swap nodes
  98. G.add_edge(start, third)
  99. G.add_edge(third, second)
  100. G.add_edge(second, fourth)
  101. G.remove_edge(start, second)
  102. G.remove_edge(second, third)
  103. G.remove_edge(third, fourth)
  104. swapcount += 1
  105. return G
  106. @py_random_state(3)
  107. def double_edge_swap(G, nswap=1, max_tries=100, seed=None):
  108. """Swap two edges in the graph while keeping the node degrees fixed.
  109. A double-edge swap removes two randomly chosen edges u-v and x-y
  110. and creates the new edges u-x and v-y::
  111. u--v u v
  112. becomes | |
  113. x--y x y
  114. If either the edge u-x or v-y already exist no swap is performed
  115. and another attempt is made to find a suitable edge pair.
  116. Parameters
  117. ----------
  118. G : graph
  119. An undirected graph
  120. nswap : integer (optional, default=1)
  121. Number of double-edge swaps to perform
  122. max_tries : integer (optional)
  123. Maximum number of attempts to swap edges
  124. seed : integer, random_state, or None (default)
  125. Indicator of random number generation state.
  126. See :ref:`Randomness<randomness>`.
  127. Returns
  128. -------
  129. G : graph
  130. The graph after double edge swaps.
  131. Raises
  132. ------
  133. NetworkXError
  134. If `G` is directed, or
  135. If `nswap` > `max_tries`, or
  136. If there are fewer than 4 nodes or 2 edges in `G`.
  137. NetworkXAlgorithmError
  138. If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made
  139. Notes
  140. -----
  141. Does not enforce any connectivity constraints.
  142. The graph G is modified in place.
  143. """
  144. if G.is_directed():
  145. raise nx.NetworkXError(
  146. "double_edge_swap() not defined for directed graphs. Use directed_edge_swap instead."
  147. )
  148. if nswap > max_tries:
  149. raise nx.NetworkXError("Number of swaps > number of tries allowed.")
  150. if len(G) < 4:
  151. raise nx.NetworkXError("Graph has fewer than four nodes.")
  152. if len(G.edges) < 2:
  153. raise nx.NetworkXError("Graph has fewer than 2 edges")
  154. # Instead of choosing uniformly at random from a generated edge list,
  155. # this algorithm chooses nonuniformly from the set of nodes with
  156. # probability weighted by degree.
  157. n = 0
  158. swapcount = 0
  159. keys, degrees = zip(*G.degree()) # keys, degree
  160. cdf = nx.utils.cumulative_distribution(degrees) # cdf of degree
  161. discrete_sequence = nx.utils.discrete_sequence
  162. while swapcount < nswap:
  163. # if random.random() < 0.5: continue # trick to avoid periodicities?
  164. # pick two random edges without creating edge list
  165. # choose source node indices from discrete distribution
  166. (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed)
  167. if ui == xi:
  168. continue # same source, skip
  169. u = keys[ui] # convert index to label
  170. x = keys[xi]
  171. # choose target uniformly from neighbors
  172. v = seed.choice(list(G[u]))
  173. y = seed.choice(list(G[x]))
  174. if v == y:
  175. continue # same target, skip
  176. if (x not in G[u]) and (y not in G[v]): # don't create parallel edges
  177. G.add_edge(u, x)
  178. G.add_edge(v, y)
  179. G.remove_edge(u, v)
  180. G.remove_edge(x, y)
  181. swapcount += 1
  182. if n >= max_tries:
  183. e = (
  184. f"Maximum number of swap attempts ({n}) exceeded "
  185. f"before desired swaps achieved ({nswap})."
  186. )
  187. raise nx.NetworkXAlgorithmError(e)
  188. n += 1
  189. return G
  190. @py_random_state(3)
  191. def connected_double_edge_swap(G, nswap=1, _window_threshold=3, seed=None):
  192. """Attempts the specified number of double-edge swaps in the graph `G`.
  193. A double-edge swap removes two randomly chosen edges `(u, v)` and `(x,
  194. y)` and creates the new edges `(u, x)` and `(v, y)`::
  195. u--v u v
  196. becomes | |
  197. x--y x y
  198. If either `(u, x)` or `(v, y)` already exist, then no swap is performed
  199. so the actual number of swapped edges is always *at most* `nswap`.
  200. Parameters
  201. ----------
  202. G : graph
  203. An undirected graph
  204. nswap : integer (optional, default=1)
  205. Number of double-edge swaps to perform
  206. _window_threshold : integer
  207. The window size below which connectedness of the graph will be checked
  208. after each swap.
  209. The "window" in this function is a dynamically updated integer that
  210. represents the number of swap attempts to make before checking if the
  211. graph remains connected. It is an optimization used to decrease the
  212. running time of the algorithm in exchange for increased complexity of
  213. implementation.
  214. If the window size is below this threshold, then the algorithm checks
  215. after each swap if the graph remains connected by checking if there is a
  216. path joining the two nodes whose edge was just removed. If the window
  217. size is above this threshold, then the algorithm performs do all the
  218. swaps in the window and only then check if the graph is still connected.
  219. seed : integer, random_state, or None (default)
  220. Indicator of random number generation state.
  221. See :ref:`Randomness<randomness>`.
  222. Returns
  223. -------
  224. int
  225. The number of successful swaps
  226. Raises
  227. ------
  228. NetworkXError
  229. If the input graph is not connected, or if the graph has fewer than four
  230. nodes.
  231. Notes
  232. -----
  233. The initial graph `G` must be connected, and the resulting graph is
  234. connected. The graph `G` is modified in place.
  235. References
  236. ----------
  237. .. [1] C. Gkantsidis and M. Mihail and E. Zegura,
  238. The Markov chain simulation method for generating connected
  239. power law random graphs, 2003.
  240. http://citeseer.ist.psu.edu/gkantsidis03markov.html
  241. """
  242. if not nx.is_connected(G):
  243. raise nx.NetworkXError("Graph not connected")
  244. if len(G) < 4:
  245. raise nx.NetworkXError("Graph has fewer than four nodes.")
  246. n = 0
  247. swapcount = 0
  248. deg = G.degree()
  249. # Label key for nodes
  250. dk = [n for n, d in G.degree()]
  251. cdf = nx.utils.cumulative_distribution([d for n, d in G.degree()])
  252. discrete_sequence = nx.utils.discrete_sequence
  253. window = 1
  254. while n < nswap:
  255. wcount = 0
  256. swapped = []
  257. # If the window is small, we just check each time whether the graph is
  258. # connected by checking if the nodes that were just separated are still
  259. # connected.
  260. if window < _window_threshold:
  261. # This Boolean keeps track of whether there was a failure or not.
  262. fail = False
  263. while wcount < window and n < nswap:
  264. # Pick two random edges without creating the edge list. Choose
  265. # source nodes from the discrete degree distribution.
  266. (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed)
  267. # If the source nodes are the same, skip this pair.
  268. if ui == xi:
  269. continue
  270. # Convert an index to a node label.
  271. u = dk[ui]
  272. x = dk[xi]
  273. # Choose targets uniformly from neighbors.
  274. v = seed.choice(list(G.neighbors(u)))
  275. y = seed.choice(list(G.neighbors(x)))
  276. # If the target nodes are the same, skip this pair.
  277. if v == y:
  278. continue
  279. if x not in G[u] and y not in G[v]:
  280. G.remove_edge(u, v)
  281. G.remove_edge(x, y)
  282. G.add_edge(u, x)
  283. G.add_edge(v, y)
  284. swapped.append((u, v, x, y))
  285. swapcount += 1
  286. n += 1
  287. # If G remains connected...
  288. if nx.has_path(G, u, v):
  289. wcount += 1
  290. # Otherwise, undo the changes.
  291. else:
  292. G.add_edge(u, v)
  293. G.add_edge(x, y)
  294. G.remove_edge(u, x)
  295. G.remove_edge(v, y)
  296. swapcount -= 1
  297. fail = True
  298. # If one of the swaps failed, reduce the window size.
  299. if fail:
  300. window = math.ceil(window / 2)
  301. else:
  302. window += 1
  303. # If the window is large, then there is a good chance that a bunch of
  304. # swaps will work. It's quicker to do all those swaps first and then
  305. # check if the graph remains connected.
  306. else:
  307. while wcount < window and n < nswap:
  308. # Pick two random edges without creating the edge list. Choose
  309. # source nodes from the discrete degree distribution.
  310. (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed)
  311. # If the source nodes are the same, skip this pair.
  312. if ui == xi:
  313. continue
  314. # Convert an index to a node label.
  315. u = dk[ui]
  316. x = dk[xi]
  317. # Choose targets uniformly from neighbors.
  318. v = seed.choice(list(G.neighbors(u)))
  319. y = seed.choice(list(G.neighbors(x)))
  320. # If the target nodes are the same, skip this pair.
  321. if v == y:
  322. continue
  323. if x not in G[u] and y not in G[v]:
  324. G.remove_edge(u, v)
  325. G.remove_edge(x, y)
  326. G.add_edge(u, x)
  327. G.add_edge(v, y)
  328. swapped.append((u, v, x, y))
  329. swapcount += 1
  330. n += 1
  331. wcount += 1
  332. # If the graph remains connected, increase the window size.
  333. if nx.is_connected(G):
  334. window += 1
  335. # Otherwise, undo the changes from the previous window and decrease
  336. # the window size.
  337. else:
  338. while swapped:
  339. (u, v, x, y) = swapped.pop()
  340. G.add_edge(u, v)
  341. G.add_edge(x, y)
  342. G.remove_edge(u, x)
  343. G.remove_edge(v, y)
  344. swapcount -= 1
  345. window = math.ceil(window / 2)
  346. return swapcount