Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/networkx/algorithms/swap.py: 9%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

166 statements  

1"""Swap edges in a graph.""" 

2 

3import math 

4 

5import networkx as nx 

6from networkx.utils import py_random_state 

7 

8__all__ = ["double_edge_swap", "connected_double_edge_swap", "directed_edge_swap"] 

9 

10 

11@nx.utils.not_implemented_for("undirected") 

12@py_random_state(3) 

13@nx._dispatchable(mutates_input=True, returns_graph=True) 

14def directed_edge_swap(G, *, nswap=1, max_tries=100, seed=None): 

15 """Swap three edges in a directed graph while keeping the node degrees fixed. 

16 

17 A directed edge swap swaps three edges such that a -> b -> c -> d becomes 

18 a -> c -> b -> d. This pattern of swapping allows all possible states with the 

19 same in- and out-degree distribution in a directed graph to be reached. 

20 

21 If the swap would create parallel edges (e.g. if a -> c already existed in the 

22 previous example), another attempt is made to find a suitable trio of edges. 

23 

24 Parameters 

25 ---------- 

26 G : DiGraph 

27 A directed graph 

28 

29 nswap : integer (optional, default=1) 

30 Number of three-edge (directed) swaps to perform 

31 

32 max_tries : integer (optional, default=100) 

33 Maximum number of attempts to swap edges 

34 

35 seed : integer, random_state, or None (default) 

36 Indicator of random number generation state. 

37 See :ref:`Randomness<randomness>`. 

38 

39 Returns 

40 ------- 

41 G : DiGraph 

42 The graph after the edges are swapped. 

43 

44 Raises 

45 ------ 

46 NetworkXError 

47 If `G` is not directed, or 

48 If nswap > max_tries, or 

49 If there are fewer than 4 nodes or 3 edges in `G`. 

50 NetworkXAlgorithmError 

51 If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made 

52 

53 Notes 

54 ----- 

55 Does not enforce any connectivity constraints. 

56 

57 The graph G is modified in place. 

58 

59 A later swap is allowed to undo a previous swap. 

60 

61 References 

62 ---------- 

63 .. [1] Erdős, Péter L., et al. “A Simple Havel-Hakimi Type Algorithm to Realize 

64 Graphical Degree Sequences of Directed Graphs.” ArXiv:0905.4913 [Math], 

65 Jan. 2010. https://doi.org/10.48550/arXiv.0905.4913. 

66 Published 2010 in Elec. J. Combinatorics (17(1)). R66. 

67 http://www.combinatorics.org/Volume_17/PDF/v17i1r66.pdf 

68 .. [2] “Combinatorics - Reaching All Possible Simple Directed Graphs with a given 

69 Degree Sequence with 2-Edge Swaps.” Mathematics Stack Exchange, 

70 https://math.stackexchange.com/questions/22272/. Accessed 30 May 2022. 

71 """ 

72 if nswap > max_tries: 

73 raise nx.NetworkXError("Number of swaps > number of tries allowed.") 

74 if len(G) < 4: 

75 raise nx.NetworkXError("DiGraph has fewer than four nodes.") 

76 if len(G.edges) < 3: 

77 raise nx.NetworkXError("DiGraph has fewer than 3 edges") 

78 

79 # Instead of choosing uniformly at random from a generated edge list, 

80 # this algorithm chooses nonuniformly from the set of nodes with 

81 # probability weighted by degree. 

82 tries = 0 

83 swapcount = 0 

84 keys, degrees = zip(*G.degree()) # keys, degree 

85 cdf = nx.utils.cumulative_distribution(degrees) # cdf of degree 

86 discrete_sequence = nx.utils.discrete_sequence 

87 

88 while swapcount < nswap: 

89 # choose source node index from discrete distribution 

90 start_index = discrete_sequence(1, cdistribution=cdf, seed=seed)[0] 

91 start = keys[start_index] 

92 tries += 1 

93 

94 if tries > max_tries: 

95 msg = f"Maximum number of swap attempts ({tries}) exceeded before desired swaps achieved ({nswap})." 

96 raise nx.NetworkXAlgorithmError(msg) 

97 

98 # If the given node doesn't have any out edges, then there isn't anything to swap 

99 if G.out_degree(start) == 0: 

100 continue 

101 second = seed.choice(list(G.succ[start])) 

102 if start == second: 

103 continue 

104 

105 if G.out_degree(second) == 0: 

106 continue 

107 third = seed.choice(list(G.succ[second])) 

108 if second == third: 

109 continue 

110 

111 if G.out_degree(third) == 0: 

112 continue 

113 fourth = seed.choice(list(G.succ[third])) 

114 if third == fourth: 

115 continue 

116 

117 if ( 

118 third not in G.succ[start] 

119 and fourth not in G.succ[second] 

120 and second not in G.succ[third] 

121 ): 

122 # Swap nodes 

123 G.add_edge(start, third) 

124 G.add_edge(third, second) 

125 G.add_edge(second, fourth) 

126 G.remove_edge(start, second) 

127 G.remove_edge(second, third) 

128 G.remove_edge(third, fourth) 

129 swapcount += 1 

130 

131 return G 

132 

133 

134@py_random_state(3) 

135@nx._dispatchable(mutates_input=True, returns_graph=True) 

136def double_edge_swap(G, nswap=1, max_tries=100, seed=None): 

137 """Swap two edges in the graph while keeping the node degrees fixed. 

138 

139 A double-edge swap removes two randomly chosen edges u-v and x-y 

140 and creates the new edges u-x and v-y:: 

141 

142 u--v u v 

143 becomes | | 

144 x--y x y 

145 

146 If either the edge u-x or v-y already exist no swap is performed 

147 and another attempt is made to find a suitable edge pair. 

148 

149 Parameters 

150 ---------- 

151 G : graph 

152 An undirected graph 

153 

154 nswap : integer (optional, default=1) 

155 Number of double-edge swaps to perform 

156 

157 max_tries : integer (optional) 

158 Maximum number of attempts to swap edges 

159 

160 seed : integer, random_state, or None (default) 

161 Indicator of random number generation state. 

162 See :ref:`Randomness<randomness>`. 

163 

164 Returns 

165 ------- 

166 G : graph 

167 The graph after double edge swaps. 

168 

169 Raises 

170 ------ 

171 NetworkXError 

172 If `G` is directed, or 

173 If `nswap` > `max_tries`, or 

174 If there are fewer than 4 nodes or 2 edges in `G`. 

175 NetworkXAlgorithmError 

176 If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made 

177 

178 Notes 

179 ----- 

180 Does not enforce any connectivity constraints. 

181 

182 The graph G is modified in place. 

183 """ 

184 if G.is_directed(): 

185 raise nx.NetworkXError( 

186 "double_edge_swap() not defined for directed graphs. Use directed_edge_swap instead." 

187 ) 

188 if nswap > max_tries: 

189 raise nx.NetworkXError("Number of swaps > number of tries allowed.") 

190 if len(G) < 4: 

191 raise nx.NetworkXError("Graph has fewer than four nodes.") 

192 if len(G.edges) < 2: 

193 raise nx.NetworkXError("Graph has fewer than 2 edges") 

194 # Instead of choosing uniformly at random from a generated edge list, 

195 # this algorithm chooses nonuniformly from the set of nodes with 

196 # probability weighted by degree. 

197 n = 0 

198 swapcount = 0 

199 keys, degrees = zip(*G.degree()) # keys, degree 

200 cdf = nx.utils.cumulative_distribution(degrees) # cdf of degree 

201 discrete_sequence = nx.utils.discrete_sequence 

202 while swapcount < nswap: 

203 # if random.random() < 0.5: continue # trick to avoid periodicities? 

204 # pick two random edges without creating edge list 

205 # choose source node indices from discrete distribution 

206 (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed) 

207 if ui == xi: 

208 continue # same source, skip 

209 u = keys[ui] # convert index to label 

210 x = keys[xi] 

211 # choose target uniformly from neighbors 

212 v = seed.choice(list(G[u])) 

213 y = seed.choice(list(G[x])) 

214 if v == y: 

215 continue # same target, skip 

216 if (x not in G[u]) and (y not in G[v]): # don't create parallel edges 

217 G.add_edge(u, x) 

218 G.add_edge(v, y) 

219 G.remove_edge(u, v) 

220 G.remove_edge(x, y) 

221 swapcount += 1 

222 if n >= max_tries: 

223 e = ( 

224 f"Maximum number of swap attempts ({n}) exceeded " 

225 f"before desired swaps achieved ({nswap})." 

226 ) 

227 raise nx.NetworkXAlgorithmError(e) 

228 n += 1 

229 return G 

230 

231 

232@py_random_state(3) 

233@nx._dispatchable(mutates_input=True) 

234def connected_double_edge_swap(G, nswap=1, _window_threshold=3, seed=None): 

235 """Attempts the specified number of double-edge swaps in the graph `G`. 

236 

237 A double-edge swap removes two randomly chosen edges `(u, v)` and `(x, 

238 y)` and creates the new edges `(u, x)` and `(v, y)`:: 

239 

240 u--v u v 

241 becomes | | 

242 x--y x y 

243 

244 If either `(u, x)` or `(v, y)` already exist, then no swap is performed 

245 so the actual number of swapped edges is always *at most* `nswap`. 

246 

247 Parameters 

248 ---------- 

249 G : graph 

250 An undirected graph 

251 

252 nswap : integer (optional, default=1) 

253 Number of double-edge swaps to perform 

254 

255 _window_threshold : integer 

256 

257 The window size below which connectedness of the graph will be checked 

258 after each swap. 

259 

260 The "window" in this function is a dynamically updated integer that 

261 represents the number of swap attempts to make before checking if the 

262 graph remains connected. It is an optimization used to decrease the 

263 running time of the algorithm in exchange for increased complexity of 

264 implementation. 

265 

266 If the window size is below this threshold, then the algorithm checks 

267 after each swap if the graph remains connected by checking if there is a 

268 path joining the two nodes whose edge was just removed. If the window 

269 size is above this threshold, then the algorithm performs do all the 

270 swaps in the window and only then check if the graph is still connected. 

271 

272 seed : integer, random_state, or None (default) 

273 Indicator of random number generation state. 

274 See :ref:`Randomness<randomness>`. 

275 

276 Returns 

277 ------- 

278 int 

279 The number of successful swaps 

280 

281 Raises 

282 ------ 

283 

284 NetworkXError 

285 

286 If the input graph is not connected, or if the graph has fewer than four 

287 nodes. 

288 

289 Notes 

290 ----- 

291 

292 The initial graph `G` must be connected, and the resulting graph is 

293 connected. The graph `G` is modified in place. 

294 

295 References 

296 ---------- 

297 .. [1] C. Gkantsidis and M. Mihail and E. Zegura, 

298 The Markov chain simulation method for generating connected 

299 power law random graphs, 2003. 

300 http://citeseer.ist.psu.edu/gkantsidis03markov.html 

301 """ 

302 if not nx.is_connected(G): 

303 raise nx.NetworkXError("Graph not connected") 

304 if len(G) < 4: 

305 raise nx.NetworkXError("Graph has fewer than four nodes.") 

306 n = 0 

307 swapcount = 0 

308 deg = G.degree() 

309 # Label key for nodes 

310 dk = [n for n, d in G.degree()] 

311 cdf = nx.utils.cumulative_distribution([d for n, d in G.degree()]) 

312 discrete_sequence = nx.utils.discrete_sequence 

313 window = 1 

314 while n < nswap: 

315 wcount = 0 

316 swapped = [] 

317 # If the window is small, we just check each time whether the graph is 

318 # connected by checking if the nodes that were just separated are still 

319 # connected. 

320 if window < _window_threshold: 

321 # This Boolean keeps track of whether there was a failure or not. 

322 fail = False 

323 while wcount < window and n < nswap: 

324 # Pick two random edges without creating the edge list. Choose 

325 # source nodes from the discrete degree distribution. 

326 (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed) 

327 # If the source nodes are the same, skip this pair. 

328 if ui == xi: 

329 continue 

330 # Convert an index to a node label. 

331 u = dk[ui] 

332 x = dk[xi] 

333 # Choose targets uniformly from neighbors. 

334 v = seed.choice(list(G.neighbors(u))) 

335 y = seed.choice(list(G.neighbors(x))) 

336 # If the target nodes are the same, skip this pair. 

337 if v == y: 

338 continue 

339 if x not in G[u] and y not in G[v]: 

340 G.remove_edge(u, v) 

341 G.remove_edge(x, y) 

342 G.add_edge(u, x) 

343 G.add_edge(v, y) 

344 swapped.append((u, v, x, y)) 

345 swapcount += 1 

346 n += 1 

347 # If G remains connected... 

348 if nx.has_path(G, u, v): 

349 wcount += 1 

350 # Otherwise, undo the changes. 

351 else: 

352 G.add_edge(u, v) 

353 G.add_edge(x, y) 

354 G.remove_edge(u, x) 

355 G.remove_edge(v, y) 

356 swapcount -= 1 

357 fail = True 

358 # If one of the swaps failed, reduce the window size. 

359 if fail: 

360 window = math.ceil(window / 2) 

361 else: 

362 window += 1 

363 # If the window is large, then there is a good chance that a bunch of 

364 # swaps will work. It's quicker to do all those swaps first and then 

365 # check if the graph remains connected. 

366 else: 

367 while wcount < window and n < nswap: 

368 # Pick two random edges without creating the edge list. Choose 

369 # source nodes from the discrete degree distribution. 

370 (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed) 

371 # If the source nodes are the same, skip this pair. 

372 if ui == xi: 

373 continue 

374 # Convert an index to a node label. 

375 u = dk[ui] 

376 x = dk[xi] 

377 # Choose targets uniformly from neighbors. 

378 v = seed.choice(list(G.neighbors(u))) 

379 y = seed.choice(list(G.neighbors(x))) 

380 # If the target nodes are the same, skip this pair. 

381 if v == y: 

382 continue 

383 if x not in G[u] and y not in G[v]: 

384 G.remove_edge(u, v) 

385 G.remove_edge(x, y) 

386 G.add_edge(u, x) 

387 G.add_edge(v, y) 

388 swapped.append((u, v, x, y)) 

389 swapcount += 1 

390 n += 1 

391 wcount += 1 

392 # If the graph remains connected, increase the window size. 

393 if nx.is_connected(G): 

394 window += 1 

395 # Otherwise, undo the changes from the previous window and decrease 

396 # the window size. 

397 else: 

398 while swapped: 

399 (u, v, x, y) = swapped.pop() 

400 G.add_edge(u, v) 

401 G.add_edge(x, y) 

402 G.remove_edge(u, x) 

403 G.remove_edge(v, y) 

404 swapcount -= 1 

405 window = math.ceil(window / 2) 

406 return swapcount