joint_degree_seq.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. """Generate graphs with a given joint degree and directed joint degree"""
  2. import networkx as nx
  3. from networkx.utils import py_random_state
  4. __all__ = [
  5. "is_valid_joint_degree",
  6. "is_valid_directed_joint_degree",
  7. "joint_degree_graph",
  8. "directed_joint_degree_graph",
  9. ]
  10. def is_valid_joint_degree(joint_degrees):
  11. """Checks whether the given joint degree dictionary is realizable.
  12. A *joint degree dictionary* is a dictionary of dictionaries, in
  13. which entry ``joint_degrees[k][l]`` is an integer representing the
  14. number of edges joining nodes of degree *k* with nodes of degree
  15. *l*. Such a dictionary is realizable as a simple graph if and only
  16. if the following conditions are satisfied.
  17. - each entry must be an integer,
  18. - the total number of nodes of degree *k*, computed by
  19. ``sum(joint_degrees[k].values()) / k``, must be an integer,
  20. - the total number of edges joining nodes of degree *k* with
  21. nodes of degree *l* cannot exceed the total number of possible edges,
  22. - each diagonal entry ``joint_degrees[k][k]`` must be even (this is
  23. a convention assumed by the :func:`joint_degree_graph` function).
  24. Parameters
  25. ----------
  26. joint_degrees : dictionary of dictionary of integers
  27. A joint degree dictionary in which entry ``joint_degrees[k][l]``
  28. is the number of edges joining nodes of degree *k* with nodes of
  29. degree *l*.
  30. Returns
  31. -------
  32. bool
  33. Whether the given joint degree dictionary is realizable as a
  34. simple graph.
  35. References
  36. ----------
  37. .. [1] M. Gjoka, M. Kurant, A. Markopoulou, "2.5K Graphs: from Sampling
  38. to Generation", IEEE Infocom, 2013.
  39. .. [2] I. Stanton, A. Pinar, "Constructing and sampling graphs with a
  40. prescribed joint degree distribution", Journal of Experimental
  41. Algorithmics, 2012.
  42. """
  43. degree_count = {}
  44. for k in joint_degrees:
  45. if k > 0:
  46. k_size = sum(joint_degrees[k].values()) / k
  47. if not k_size.is_integer():
  48. return False
  49. degree_count[k] = k_size
  50. for k in joint_degrees:
  51. for l in joint_degrees[k]:
  52. if not float(joint_degrees[k][l]).is_integer():
  53. return False
  54. if (k != l) and (joint_degrees[k][l] > degree_count[k] * degree_count[l]):
  55. return False
  56. elif k == l:
  57. if joint_degrees[k][k] > degree_count[k] * (degree_count[k] - 1):
  58. return False
  59. if joint_degrees[k][k] % 2 != 0:
  60. return False
  61. # if all above conditions have been satisfied then the input
  62. # joint degree is realizable as a simple graph.
  63. return True
  64. def _neighbor_switch(G, w, unsat, h_node_residual, avoid_node_id=None):
  65. """Releases one free stub for ``w``, while preserving joint degree in G.
  66. Parameters
  67. ----------
  68. G : NetworkX graph
  69. Graph in which the neighbor switch will take place.
  70. w : integer
  71. Node id for which we will execute this neighbor switch.
  72. unsat : set of integers
  73. Set of unsaturated node ids that have the same degree as w.
  74. h_node_residual: dictionary of integers
  75. Keeps track of the remaining stubs for a given node.
  76. avoid_node_id: integer
  77. Node id to avoid when selecting w_prime.
  78. Notes
  79. -----
  80. First, it selects *w_prime*, an unsaturated node that has the same degree
  81. as ``w``. Second, it selects *switch_node*, a neighbor node of ``w`` that
  82. is not connected to *w_prime*. Then it executes an edge swap i.e. removes
  83. (``w``,*switch_node*) and adds (*w_prime*,*switch_node*). Gjoka et. al. [1]
  84. prove that such an edge swap is always possible.
  85. References
  86. ----------
  87. .. [1] M. Gjoka, B. Tillman, A. Markopoulou, "Construction of Simple
  88. Graphs with a Target Joint Degree Matrix and Beyond", IEEE Infocom, '15
  89. """
  90. if (avoid_node_id is None) or (h_node_residual[avoid_node_id] > 1):
  91. # select unsatured node w_prime that has the same degree as w
  92. w_prime = next(iter(unsat))
  93. else:
  94. # assume that the node pair (v,w) has been selected for connection. if
  95. # - neighbor_switch is called for node w,
  96. # - nodes v and w have the same degree,
  97. # - node v=avoid_node_id has only one stub left,
  98. # then prevent v=avoid_node_id from being selected as w_prime.
  99. iter_var = iter(unsat)
  100. while True:
  101. w_prime = next(iter_var)
  102. if w_prime != avoid_node_id:
  103. break
  104. # select switch_node, a neighbor of w, that is not connected to w_prime
  105. w_prime_neighbs = G[w_prime] # slightly faster declaring this variable
  106. for v in G[w]:
  107. if (v not in w_prime_neighbs) and (v != w_prime):
  108. switch_node = v
  109. break
  110. # remove edge (w,switch_node), add edge (w_prime,switch_node) and update
  111. # data structures
  112. G.remove_edge(w, switch_node)
  113. G.add_edge(w_prime, switch_node)
  114. h_node_residual[w] += 1
  115. h_node_residual[w_prime] -= 1
  116. if h_node_residual[w_prime] == 0:
  117. unsat.remove(w_prime)
  118. @py_random_state(1)
  119. def joint_degree_graph(joint_degrees, seed=None):
  120. """Generates a random simple graph with the given joint degree dictionary.
  121. Parameters
  122. ----------
  123. joint_degrees : dictionary of dictionary of integers
  124. A joint degree dictionary in which entry ``joint_degrees[k][l]`` is the
  125. number of edges joining nodes of degree *k* with nodes of degree *l*.
  126. seed : integer, random_state, or None (default)
  127. Indicator of random number generation state.
  128. See :ref:`Randomness<randomness>`.
  129. Returns
  130. -------
  131. G : Graph
  132. A graph with the specified joint degree dictionary.
  133. Raises
  134. ------
  135. NetworkXError
  136. If *joint_degrees* dictionary is not realizable.
  137. Notes
  138. -----
  139. In each iteration of the "while loop" the algorithm picks two disconnected
  140. nodes *v* and *w*, of degree *k* and *l* correspondingly, for which
  141. ``joint_degrees[k][l]`` has not reached its target yet. It then adds
  142. edge (*v*, *w*) and increases the number of edges in graph G by one.
  143. The intelligence of the algorithm lies in the fact that it is always
  144. possible to add an edge between such disconnected nodes *v* and *w*,
  145. even if one or both nodes do not have free stubs. That is made possible by
  146. executing a "neighbor switch", an edge rewiring move that releases
  147. a free stub while keeping the joint degree of G the same.
  148. The algorithm continues for E (number of edges) iterations of
  149. the "while loop", at the which point all entries of the given
  150. ``joint_degrees[k][l]`` have reached their target values and the
  151. construction is complete.
  152. References
  153. ----------
  154. .. [1] M. Gjoka, B. Tillman, A. Markopoulou, "Construction of Simple
  155. Graphs with a Target Joint Degree Matrix and Beyond", IEEE Infocom, '15
  156. Examples
  157. --------
  158. >>> joint_degrees = {
  159. ... 1: {4: 1},
  160. ... 2: {2: 2, 3: 2, 4: 2},
  161. ... 3: {2: 2, 4: 1},
  162. ... 4: {1: 1, 2: 2, 3: 1},
  163. ... }
  164. >>> G = nx.joint_degree_graph(joint_degrees)
  165. >>>
  166. """
  167. if not is_valid_joint_degree(joint_degrees):
  168. msg = "Input joint degree dict not realizable as a simple graph"
  169. raise nx.NetworkXError(msg)
  170. # compute degree count from joint_degrees
  171. degree_count = {k: sum(l.values()) // k for k, l in joint_degrees.items() if k > 0}
  172. # start with empty N-node graph
  173. N = sum(degree_count.values())
  174. G = nx.empty_graph(N)
  175. # for a given degree group, keep the list of all node ids
  176. h_degree_nodelist = {}
  177. # for a given node, keep track of the remaining stubs
  178. h_node_residual = {}
  179. # populate h_degree_nodelist and h_node_residual
  180. nodeid = 0
  181. for degree, num_nodes in degree_count.items():
  182. h_degree_nodelist[degree] = range(nodeid, nodeid + num_nodes)
  183. for v in h_degree_nodelist[degree]:
  184. h_node_residual[v] = degree
  185. nodeid += int(num_nodes)
  186. # iterate over every degree pair (k,l) and add the number of edges given
  187. # for each pair
  188. for k in joint_degrees:
  189. for l in joint_degrees[k]:
  190. # n_edges_add is the number of edges to add for the
  191. # degree pair (k,l)
  192. n_edges_add = joint_degrees[k][l]
  193. if (n_edges_add > 0) and (k >= l):
  194. # number of nodes with degree k and l
  195. k_size = degree_count[k]
  196. l_size = degree_count[l]
  197. # k_nodes and l_nodes consist of all nodes of degree k and l
  198. k_nodes = h_degree_nodelist[k]
  199. l_nodes = h_degree_nodelist[l]
  200. # k_unsat and l_unsat consist of nodes of degree k and l that
  201. # are unsaturated (nodes that have at least 1 available stub)
  202. k_unsat = {v for v in k_nodes if h_node_residual[v] > 0}
  203. if k != l:
  204. l_unsat = {w for w in l_nodes if h_node_residual[w] > 0}
  205. else:
  206. l_unsat = k_unsat
  207. n_edges_add = joint_degrees[k][l] // 2
  208. while n_edges_add > 0:
  209. # randomly pick nodes v and w that have degrees k and l
  210. v = k_nodes[seed.randrange(k_size)]
  211. w = l_nodes[seed.randrange(l_size)]
  212. # if nodes v and w are disconnected then attempt to connect
  213. if not G.has_edge(v, w) and (v != w):
  214. # if node v has no free stubs then do neighbor switch
  215. if h_node_residual[v] == 0:
  216. _neighbor_switch(G, v, k_unsat, h_node_residual)
  217. # if node w has no free stubs then do neighbor switch
  218. if h_node_residual[w] == 0:
  219. if k != l:
  220. _neighbor_switch(G, w, l_unsat, h_node_residual)
  221. else:
  222. _neighbor_switch(
  223. G, w, l_unsat, h_node_residual, avoid_node_id=v
  224. )
  225. # add edge (v, w) and update data structures
  226. G.add_edge(v, w)
  227. h_node_residual[v] -= 1
  228. h_node_residual[w] -= 1
  229. n_edges_add -= 1
  230. if h_node_residual[v] == 0:
  231. k_unsat.discard(v)
  232. if h_node_residual[w] == 0:
  233. l_unsat.discard(w)
  234. return G
  235. def is_valid_directed_joint_degree(in_degrees, out_degrees, nkk):
  236. """Checks whether the given directed joint degree input is realizable
  237. Parameters
  238. ----------
  239. in_degrees : list of integers
  240. in degree sequence contains the in degrees of nodes.
  241. out_degrees : list of integers
  242. out degree sequence contains the out degrees of nodes.
  243. nkk : dictionary of dictionary of integers
  244. directed joint degree dictionary. for nodes of out degree k (first
  245. level of dict) and nodes of in degree l (seconnd level of dict)
  246. describes the number of edges.
  247. Returns
  248. -------
  249. boolean
  250. returns true if given input is realizable, else returns false.
  251. Notes
  252. -----
  253. Here is the list of conditions that the inputs (in/out degree sequences,
  254. nkk) need to satisfy for simple directed graph realizability:
  255. - Condition 0: in_degrees and out_degrees have the same length
  256. - Condition 1: nkk[k][l] is integer for all k,l
  257. - Condition 2: sum(nkk[k])/k = number of nodes with partition id k, is an
  258. integer and matching degree sequence
  259. - Condition 3: number of edges and non-chords between k and l cannot exceed
  260. maximum possible number of edges
  261. References
  262. ----------
  263. [1] B. Tillman, A. Markopoulou, C. T. Butts & M. Gjoka,
  264. "Construction of Directed 2K Graphs". In Proc. of KDD 2017.
  265. """
  266. V = {} # number of nodes with in/out degree.
  267. forbidden = {}
  268. if len(in_degrees) != len(out_degrees):
  269. return False
  270. for idx in range(0, len(in_degrees)):
  271. i = in_degrees[idx]
  272. o = out_degrees[idx]
  273. V[(i, 0)] = V.get((i, 0), 0) + 1
  274. V[(o, 1)] = V.get((o, 1), 0) + 1
  275. forbidden[(o, i)] = forbidden.get((o, i), 0) + 1
  276. S = {} # number of edges going from in/out degree nodes.
  277. for k in nkk:
  278. for l in nkk[k]:
  279. val = nkk[k][l]
  280. if not float(val).is_integer(): # condition 1
  281. return False
  282. if val > 0:
  283. S[(k, 1)] = S.get((k, 1), 0) + val
  284. S[(l, 0)] = S.get((l, 0), 0) + val
  285. # condition 3
  286. if val + forbidden.get((k, l), 0) > V[(k, 1)] * V[(l, 0)]:
  287. return False
  288. return all(S[s] / s[0] == V[s] for s in S)
  289. def _directed_neighbor_switch(
  290. G, w, unsat, h_node_residual_out, chords, h_partition_in, partition
  291. ):
  292. """Releases one free stub for node w, while preserving joint degree in G.
  293. Parameters
  294. ----------
  295. G : networkx directed graph
  296. graph within which the edge swap will take place.
  297. w : integer
  298. node id for which we need to perform a neighbor switch.
  299. unsat: set of integers
  300. set of node ids that have the same degree as w and are unsaturated.
  301. h_node_residual_out: dict of integers
  302. for a given node, keeps track of the remaining stubs to be added.
  303. chords: set of tuples
  304. keeps track of available positions to add edges.
  305. h_partition_in: dict of integers
  306. for a given node, keeps track of its partition id (in degree).
  307. partition: integer
  308. partition id to check if chords have to be updated.
  309. Notes
  310. -----
  311. First, it selects node w_prime that (1) has the same degree as w and
  312. (2) is unsaturated. Then, it selects node v, a neighbor of w, that is
  313. not connected to w_prime and does an edge swap i.e. removes (w,v) and
  314. adds (w_prime,v). If neighbor switch is not possible for w using
  315. w_prime and v, then return w_prime; in [1] it's proven that
  316. such unsaturated nodes can be used.
  317. References
  318. ----------
  319. [1] B. Tillman, A. Markopoulou, C. T. Butts & M. Gjoka,
  320. "Construction of Directed 2K Graphs". In Proc. of KDD 2017.
  321. """
  322. w_prime = unsat.pop()
  323. unsat.add(w_prime)
  324. # select node t, a neighbor of w, that is not connected to w_prime
  325. w_neighbs = list(G.successors(w))
  326. # slightly faster declaring this variable
  327. w_prime_neighbs = list(G.successors(w_prime))
  328. for v in w_neighbs:
  329. if (v not in w_prime_neighbs) and w_prime != v:
  330. # removes (w,v), add (w_prime,v) and update data structures
  331. G.remove_edge(w, v)
  332. G.add_edge(w_prime, v)
  333. if h_partition_in[v] == partition:
  334. chords.add((w, v))
  335. chords.discard((w_prime, v))
  336. h_node_residual_out[w] += 1
  337. h_node_residual_out[w_prime] -= 1
  338. if h_node_residual_out[w_prime] == 0:
  339. unsat.remove(w_prime)
  340. return None
  341. # If neighbor switch didn't work, use unsaturated node
  342. return w_prime
  343. def _directed_neighbor_switch_rev(
  344. G, w, unsat, h_node_residual_in, chords, h_partition_out, partition
  345. ):
  346. """The reverse of directed_neighbor_switch.
  347. Parameters
  348. ----------
  349. G : networkx directed graph
  350. graph within which the edge swap will take place.
  351. w : integer
  352. node id for which we need to perform a neighbor switch.
  353. unsat: set of integers
  354. set of node ids that have the same degree as w and are unsaturated.
  355. h_node_residual_in: dict of integers
  356. for a given node, keeps track of the remaining stubs to be added.
  357. chords: set of tuples
  358. keeps track of available positions to add edges.
  359. h_partition_out: dict of integers
  360. for a given node, keeps track of its partition id (out degree).
  361. partition: integer
  362. partition id to check if chords have to be updated.
  363. Notes
  364. -----
  365. Same operation as directed_neighbor_switch except it handles this operation
  366. for incoming edges instead of outgoing.
  367. """
  368. w_prime = unsat.pop()
  369. unsat.add(w_prime)
  370. # slightly faster declaring these as variables.
  371. w_neighbs = list(G.predecessors(w))
  372. w_prime_neighbs = list(G.predecessors(w_prime))
  373. # select node v, a neighbor of w, that is not connected to w_prime.
  374. for v in w_neighbs:
  375. if (v not in w_prime_neighbs) and w_prime != v:
  376. # removes (v,w), add (v,w_prime) and update data structures.
  377. G.remove_edge(v, w)
  378. G.add_edge(v, w_prime)
  379. if h_partition_out[v] == partition:
  380. chords.add((v, w))
  381. chords.discard((v, w_prime))
  382. h_node_residual_in[w] += 1
  383. h_node_residual_in[w_prime] -= 1
  384. if h_node_residual_in[w_prime] == 0:
  385. unsat.remove(w_prime)
  386. return None
  387. # If neighbor switch didn't work, use the unsaturated node.
  388. return w_prime
  389. @py_random_state(3)
  390. def directed_joint_degree_graph(in_degrees, out_degrees, nkk, seed=None):
  391. """Generates a random simple directed graph with the joint degree.
  392. Parameters
  393. ----------
  394. degree_seq : list of tuples (of size 3)
  395. degree sequence contains tuples of nodes with node id, in degree and
  396. out degree.
  397. nkk : dictionary of dictionary of integers
  398. directed joint degree dictionary, for nodes of out degree k (first
  399. level of dict) and nodes of in degree l (second level of dict)
  400. describes the number of edges.
  401. seed : hashable object, optional
  402. Seed for random number generator.
  403. Returns
  404. -------
  405. G : Graph
  406. A directed graph with the specified inputs.
  407. Raises
  408. ------
  409. NetworkXError
  410. If degree_seq and nkk are not realizable as a simple directed graph.
  411. Notes
  412. -----
  413. Similarly to the undirected version:
  414. In each iteration of the "while loop" the algorithm picks two disconnected
  415. nodes v and w, of degree k and l correspondingly, for which nkk[k][l] has
  416. not reached its target yet i.e. (for given k,l): n_edges_add < nkk[k][l].
  417. It then adds edge (v,w) and always increases the number of edges in graph G
  418. by one.
  419. The intelligence of the algorithm lies in the fact that it is always
  420. possible to add an edge between disconnected nodes v and w, for which
  421. nkk[degree(v)][degree(w)] has not reached its target, even if one or both
  422. nodes do not have free stubs. If either node v or w does not have a free
  423. stub, we perform a "neighbor switch", an edge rewiring move that releases a
  424. free stub while keeping nkk the same.
  425. The difference for the directed version lies in the fact that neighbor
  426. switches might not be able to rewire, but in these cases unsaturated nodes
  427. can be reassigned to use instead, see [1] for detailed description and
  428. proofs.
  429. The algorithm continues for E (number of edges in the graph) iterations of
  430. the "while loop", at which point all entries of the given nkk[k][l] have
  431. reached their target values and the construction is complete.
  432. References
  433. ----------
  434. [1] B. Tillman, A. Markopoulou, C. T. Butts & M. Gjoka,
  435. "Construction of Directed 2K Graphs". In Proc. of KDD 2017.
  436. Examples
  437. --------
  438. >>> in_degrees = [0, 1, 1, 2]
  439. >>> out_degrees = [1, 1, 1, 1]
  440. >>> nkk = {1: {1: 2, 2: 2}}
  441. >>> G = nx.directed_joint_degree_graph(in_degrees, out_degrees, nkk)
  442. >>>
  443. """
  444. if not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk):
  445. msg = "Input is not realizable as a simple graph"
  446. raise nx.NetworkXError(msg)
  447. # start with an empty directed graph.
  448. G = nx.DiGraph()
  449. # for a given group, keep the list of all node ids.
  450. h_degree_nodelist_in = {}
  451. h_degree_nodelist_out = {}
  452. # for a given group, keep the list of all unsaturated node ids.
  453. h_degree_nodelist_in_unsat = {}
  454. h_degree_nodelist_out_unsat = {}
  455. # for a given node, keep track of the remaining stubs to be added.
  456. h_node_residual_out = {}
  457. h_node_residual_in = {}
  458. # for a given node, keep track of the partition id.
  459. h_partition_out = {}
  460. h_partition_in = {}
  461. # keep track of non-chords between pairs of partition ids.
  462. non_chords = {}
  463. # populate data structures
  464. for idx, i in enumerate(in_degrees):
  465. idx = int(idx)
  466. if i > 0:
  467. h_degree_nodelist_in.setdefault(i, [])
  468. h_degree_nodelist_in_unsat.setdefault(i, set())
  469. h_degree_nodelist_in[i].append(idx)
  470. h_degree_nodelist_in_unsat[i].add(idx)
  471. h_node_residual_in[idx] = i
  472. h_partition_in[idx] = i
  473. for idx, o in enumerate(out_degrees):
  474. o = out_degrees[idx]
  475. non_chords[(o, in_degrees[idx])] = non_chords.get((o, in_degrees[idx]), 0) + 1
  476. idx = int(idx)
  477. if o > 0:
  478. h_degree_nodelist_out.setdefault(o, [])
  479. h_degree_nodelist_out_unsat.setdefault(o, set())
  480. h_degree_nodelist_out[o].append(idx)
  481. h_degree_nodelist_out_unsat[o].add(idx)
  482. h_node_residual_out[idx] = o
  483. h_partition_out[idx] = o
  484. G.add_node(idx)
  485. nk_in = {}
  486. nk_out = {}
  487. for p in h_degree_nodelist_in:
  488. nk_in[p] = len(h_degree_nodelist_in[p])
  489. for p in h_degree_nodelist_out:
  490. nk_out[p] = len(h_degree_nodelist_out[p])
  491. # iterate over every degree pair (k,l) and add the number of edges given
  492. # for each pair.
  493. for k in nkk:
  494. for l in nkk[k]:
  495. n_edges_add = nkk[k][l]
  496. if n_edges_add > 0:
  497. # chords contains a random set of potential edges.
  498. chords = set()
  499. k_len = nk_out[k]
  500. l_len = nk_in[l]
  501. chords_sample = seed.sample(
  502. range(k_len * l_len), n_edges_add + non_chords.get((k, l), 0)
  503. )
  504. num = 0
  505. while len(chords) < n_edges_add:
  506. i = h_degree_nodelist_out[k][chords_sample[num] % k_len]
  507. j = h_degree_nodelist_in[l][chords_sample[num] // k_len]
  508. num += 1
  509. if i != j:
  510. chords.add((i, j))
  511. # k_unsat and l_unsat consist of nodes of in/out degree k and l
  512. # that are unsaturated i.e. those nodes that have at least one
  513. # available stub
  514. k_unsat = h_degree_nodelist_out_unsat[k]
  515. l_unsat = h_degree_nodelist_in_unsat[l]
  516. while n_edges_add > 0:
  517. v, w = chords.pop()
  518. chords.add((v, w))
  519. # if node v has no free stubs then do neighbor switch.
  520. if h_node_residual_out[v] == 0:
  521. _v = _directed_neighbor_switch(
  522. G,
  523. v,
  524. k_unsat,
  525. h_node_residual_out,
  526. chords,
  527. h_partition_in,
  528. l,
  529. )
  530. if _v is not None:
  531. v = _v
  532. # if node w has no free stubs then do neighbor switch.
  533. if h_node_residual_in[w] == 0:
  534. _w = _directed_neighbor_switch_rev(
  535. G,
  536. w,
  537. l_unsat,
  538. h_node_residual_in,
  539. chords,
  540. h_partition_out,
  541. k,
  542. )
  543. if _w is not None:
  544. w = _w
  545. # add edge (v,w) and update data structures.
  546. G.add_edge(v, w)
  547. h_node_residual_out[v] -= 1
  548. h_node_residual_in[w] -= 1
  549. n_edges_add -= 1
  550. chords.discard((v, w))
  551. if h_node_residual_out[v] == 0:
  552. k_unsat.discard(v)
  553. if h_node_residual_in[w] == 0:
  554. l_unsat.discard(w)
  555. return G