comparison env/lib/python3.9/site-packages/networkx/generators/joint_degree_seq.py @ 0:4f3585e2f14b draft default tip

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