1"""Generate graphs with a given degree sequence or expected degree sequence."""
2
3import heapq
4import math
5from itertools import chain, combinations, zip_longest
6from operator import itemgetter
7
8import networkx as nx
9from networkx.utils import py_random_state, random_weighted_sample
10
11__all__ = [
12 "configuration_model",
13 "directed_configuration_model",
14 "expected_degree_graph",
15 "havel_hakimi_graph",
16 "directed_havel_hakimi_graph",
17 "degree_sequence_tree",
18 "random_degree_sequence_graph",
19]
20
21chaini = chain.from_iterable
22
23
24def _to_stublist(degree_sequence):
25 """Returns a list of degree-repeated node numbers.
26
27 ``degree_sequence`` is a list of nonnegative integers representing
28 the degrees of nodes in a graph.
29
30 This function returns a list of node numbers with multiplicities
31 according to the given degree sequence. For example, if the first
32 element of ``degree_sequence`` is ``3``, then the first node number,
33 ``0``, will appear at the head of the returned list three times. The
34 node numbers are assumed to be the numbers zero through
35 ``len(degree_sequence) - 1``.
36
37 Examples
38 --------
39
40 >>> degree_sequence = [1, 2, 3]
41 >>> _to_stublist(degree_sequence)
42 [0, 1, 1, 2, 2, 2]
43
44 If a zero appears in the sequence, that means the node exists but
45 has degree zero, so that number will be skipped in the returned
46 list::
47
48 >>> degree_sequence = [2, 0, 1]
49 >>> _to_stublist(degree_sequence)
50 [0, 0, 2]
51
52 """
53 return list(chaini([n] * d for n, d in enumerate(degree_sequence)))
54
55
56def _configuration_model(
57 deg_sequence, create_using, directed=False, in_deg_sequence=None, seed=None
58):
59 """Helper function for generating either undirected or directed
60 configuration model graphs.
61
62 ``deg_sequence`` is a list of nonnegative integers representing the
63 degree of the node whose label is the index of the list element.
64
65 ``create_using`` see :func:`~networkx.empty_graph`.
66
67 ``directed`` and ``in_deg_sequence`` are required if you want the
68 returned graph to be generated using the directed configuration
69 model algorithm. If ``directed`` is ``False``, then ``deg_sequence``
70 is interpreted as the degree sequence of an undirected graph and
71 ``in_deg_sequence`` is ignored. Otherwise, if ``directed`` is
72 ``True``, then ``deg_sequence`` is interpreted as the out-degree
73 sequence and ``in_deg_sequence`` as the in-degree sequence of a
74 directed graph.
75
76 .. note::
77
78 ``deg_sequence`` and ``in_deg_sequence`` need not be the same
79 length.
80
81 ``seed`` is a random.Random or numpy.random.RandomState instance
82
83 This function returns a graph, directed if and only if ``directed``
84 is ``True``, generated according to the configuration model
85 algorithm. For more information on the algorithm, see the
86 :func:`configuration_model` or :func:`directed_configuration_model`
87 functions.
88
89 """
90 n = len(deg_sequence)
91 G = nx.empty_graph(n, create_using)
92 # If empty, return the null graph immediately.
93 if n == 0:
94 return G
95 # Build a list of available degree-repeated nodes. For example,
96 # for degree sequence [3, 2, 1, 1, 1], the "stub list" is
97 # initially [0, 0, 0, 1, 1, 2, 3, 4], that is, node 0 has degree
98 # 3 and thus is repeated 3 times, etc.
99 #
100 # Also, shuffle the stub list in order to get a random sequence of
101 # node pairs.
102 if directed:
103 pairs = zip_longest(deg_sequence, in_deg_sequence, fillvalue=0)
104 # Unzip the list of pairs into a pair of lists.
105 out_deg, in_deg = zip(*pairs)
106
107 out_stublist = _to_stublist(out_deg)
108 in_stublist = _to_stublist(in_deg)
109
110 seed.shuffle(out_stublist)
111 seed.shuffle(in_stublist)
112 else:
113 stublist = _to_stublist(deg_sequence)
114 # Choose a random balanced bipartition of the stublist, which
115 # gives a random pairing of nodes. In this implementation, we
116 # shuffle the list and then split it in half.
117 n = len(stublist)
118 half = n // 2
119 seed.shuffle(stublist)
120 out_stublist, in_stublist = stublist[:half], stublist[half:]
121 G.add_edges_from(zip(out_stublist, in_stublist))
122 return G
123
124
125@py_random_state(2)
126@nx._dispatchable(graphs=None, returns_graph=True)
127def configuration_model(deg_sequence, create_using=None, seed=None):
128 """Returns a random graph with the given degree sequence.
129
130 The configuration model generates a random pseudograph (graph with
131 parallel edges and self loops) by randomly assigning edges to
132 match the given degree sequence.
133
134 Parameters
135 ----------
136 deg_sequence : list of nonnegative integers
137 Each list entry corresponds to the degree of a node.
138 create_using : NetworkX graph constructor, optional (default MultiGraph)
139 Graph type to create. If graph instance, then cleared before populated.
140 seed : integer, random_state, or None (default)
141 Indicator of random number generation state.
142 See :ref:`Randomness<randomness>`.
143
144 Returns
145 -------
146 G : MultiGraph
147 A graph with the specified degree sequence.
148 Nodes are labeled starting at 0 with an index
149 corresponding to the position in deg_sequence.
150
151 Raises
152 ------
153 NetworkXError
154 If the degree sequence does not have an even sum.
155
156 See Also
157 --------
158 is_graphical
159
160 Notes
161 -----
162 As described by Newman [1]_.
163
164 A non-graphical degree sequence (not realizable by some simple
165 graph) is allowed since this function returns graphs with self
166 loops and parallel edges. An exception is raised if the degree
167 sequence does not have an even sum.
168
169 This configuration model construction process can lead to
170 duplicate edges and loops. You can remove the self-loops and
171 parallel edges (see below) which will likely result in a graph
172 that doesn't have the exact degree sequence specified.
173
174 The density of self-loops and parallel edges tends to decrease as
175 the number of nodes increases. However, typically the number of
176 self-loops will approach a Poisson distribution with a nonzero mean,
177 and similarly for the number of parallel edges. Consider a node
178 with *k* stubs. The probability of being joined to another stub of
179 the same node is basically (*k* - *1*) / *N*, where *k* is the
180 degree and *N* is the number of nodes. So the probability of a
181 self-loop scales like *c* / *N* for some constant *c*. As *N* grows,
182 this means we expect *c* self-loops. Similarly for parallel edges.
183
184 References
185 ----------
186 .. [1] M.E.J. Newman, "The structure and function of complex networks",
187 SIAM REVIEW 45-2, pp 167-256, 2003.
188
189 Examples
190 --------
191 You can create a degree sequence following a particular distribution
192 by using the one of the distribution functions in
193 :mod:`~networkx.utils.random_sequence` (or one of your own). For
194 example, to create an undirected multigraph on one hundred nodes
195 with degree sequence chosen from the power law distribution:
196
197 >>> sequence = nx.random_powerlaw_tree_sequence(100, tries=5000)
198 >>> G = nx.configuration_model(sequence)
199 >>> len(G)
200 100
201 >>> actual_degrees = [d for v, d in G.degree()]
202 >>> actual_degrees == sequence
203 True
204
205 The returned graph is a multigraph, which may have parallel
206 edges. To remove any parallel edges from the returned graph:
207
208 >>> G = nx.Graph(G)
209
210 Similarly, to remove self-loops:
211
212 >>> G.remove_edges_from(nx.selfloop_edges(G))
213
214 """
215 if sum(deg_sequence) % 2 != 0:
216 msg = "Invalid degree sequence: sum of degrees must be even, not odd"
217 raise nx.NetworkXError(msg)
218
219 G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
220 if G.is_directed():
221 raise nx.NetworkXNotImplemented("not implemented for directed graphs")
222
223 G = _configuration_model(deg_sequence, G, seed=seed)
224
225 return G
226
227
228@py_random_state(3)
229@nx._dispatchable(graphs=None, returns_graph=True)
230def directed_configuration_model(
231 in_degree_sequence, out_degree_sequence, create_using=None, seed=None
232):
233 """Returns a directed_random graph with the given degree sequences.
234
235 The configuration model generates a random directed pseudograph
236 (graph with parallel edges and self loops) by randomly assigning
237 edges to match the given degree sequences.
238
239 Parameters
240 ----------
241 in_degree_sequence : list of nonnegative integers
242 Each list entry corresponds to the in-degree of a node.
243 out_degree_sequence : list of nonnegative integers
244 Each list entry corresponds to the out-degree of a node.
245 create_using : NetworkX graph constructor, optional (default MultiDiGraph)
246 Graph type to create. If graph instance, then cleared before populated.
247 seed : integer, random_state, or None (default)
248 Indicator of random number generation state.
249 See :ref:`Randomness<randomness>`.
250
251 Returns
252 -------
253 G : MultiDiGraph
254 A graph with the specified degree sequences.
255 Nodes are labeled starting at 0 with an index
256 corresponding to the position in deg_sequence.
257
258 Raises
259 ------
260 NetworkXError
261 If the degree sequences do not have the same sum.
262
263 See Also
264 --------
265 configuration_model
266
267 Notes
268 -----
269 Algorithm as described by Newman [1]_.
270
271 A non-graphical degree sequence (not realizable by some simple
272 graph) is allowed since this function returns graphs with self
273 loops and parallel edges. An exception is raised if the degree
274 sequences does not have the same sum.
275
276 This configuration model construction process can lead to
277 duplicate edges and loops. You can remove the self-loops and
278 parallel edges (see below) which will likely result in a graph
279 that doesn't have the exact degree sequence specified. This
280 "finite-size effect" decreases as the size of the graph increases.
281
282 References
283 ----------
284 .. [1] Newman, M. E. J. and Strogatz, S. H. and Watts, D. J.
285 Random graphs with arbitrary degree distributions and their applications
286 Phys. Rev. E, 64, 026118 (2001)
287
288 Examples
289 --------
290 One can modify the in- and out-degree sequences from an existing
291 directed graph in order to create a new directed graph. For example,
292 here we modify the directed path graph:
293
294 >>> D = nx.DiGraph([(0, 1), (1, 2), (2, 3)])
295 >>> din = list(d for n, d in D.in_degree())
296 >>> dout = list(d for n, d in D.out_degree())
297 >>> din.append(1)
298 >>> dout[0] = 2
299 >>> # We now expect an edge from node 0 to a new node, node 3.
300 ... D = nx.directed_configuration_model(din, dout)
301
302 The returned graph is a directed multigraph, which may have parallel
303 edges. To remove any parallel edges from the returned graph:
304
305 >>> D = nx.DiGraph(D)
306
307 Similarly, to remove self-loops:
308
309 >>> D.remove_edges_from(nx.selfloop_edges(D))
310
311 """
312 if sum(in_degree_sequence) != sum(out_degree_sequence):
313 msg = "Invalid degree sequences: sequences must have equal sums"
314 raise nx.NetworkXError(msg)
315
316 if create_using is None:
317 create_using = nx.MultiDiGraph
318
319 G = _configuration_model(
320 out_degree_sequence,
321 create_using,
322 directed=True,
323 in_deg_sequence=in_degree_sequence,
324 seed=seed,
325 )
326
327 name = "directed configuration_model {} nodes {} edges"
328 return G
329
330
331@py_random_state(1)
332@nx._dispatchable(graphs=None, returns_graph=True)
333def expected_degree_graph(w, seed=None, selfloops=True):
334 r"""Returns a random graph with given expected degrees.
335
336 Given a sequence of expected degrees $W=(w_0,w_1,\ldots,w_{n-1})$
337 of length $n$ this algorithm assigns an edge between node $u$ and
338 node $v$ with probability
339
340 .. math::
341
342 p_{uv} = \frac{w_u w_v}{\sum_k w_k} .
343
344 Parameters
345 ----------
346 w : list
347 The list of expected degrees.
348 selfloops: bool (default=True)
349 Set to False to remove the possibility of self-loop edges.
350 seed : integer, random_state, or None (default)
351 Indicator of random number generation state.
352 See :ref:`Randomness<randomness>`.
353
354 Returns
355 -------
356 Graph
357
358 Examples
359 --------
360 >>> z = [10 for i in range(100)]
361 >>> G = nx.expected_degree_graph(z)
362
363 Notes
364 -----
365 The nodes have integer labels corresponding to index of expected degrees
366 input sequence.
367
368 The complexity of this algorithm is $\mathcal{O}(n+m)$ where $n$ is the
369 number of nodes and $m$ is the expected number of edges.
370
371 The model in [1]_ includes the possibility of self-loop edges.
372 Set selfloops=False to produce a graph without self loops.
373
374 For finite graphs this model doesn't produce exactly the given
375 expected degree sequence. Instead the expected degrees are as
376 follows.
377
378 For the case without self loops (selfloops=False),
379
380 .. math::
381
382 E[deg(u)] = \sum_{v \ne u} p_{uv}
383 = w_u \left( 1 - \frac{w_u}{\sum_k w_k} \right) .
384
385
386 NetworkX uses the standard convention that a self-loop edge counts 2
387 in the degree of a node, so with self loops (selfloops=True),
388
389 .. math::
390
391 E[deg(u)] = \sum_{v \ne u} p_{uv} + 2 p_{uu}
392 = w_u \left( 1 + \frac{w_u}{\sum_k w_k} \right) .
393
394 References
395 ----------
396 .. [1] Fan Chung and L. Lu, Connected components in random graphs with
397 given expected degree sequences, Ann. Combinatorics, 6,
398 pp. 125-145, 2002.
399 .. [2] Joel Miller and Aric Hagberg,
400 Efficient generation of networks with given expected degrees,
401 in Algorithms and Models for the Web-Graph (WAW 2011),
402 Alan Frieze, Paul Horn, and Paweł Prałat (Eds), LNCS 6732,
403 pp. 115-126, 2011.
404 """
405 n = len(w)
406 G = nx.empty_graph(n)
407
408 # If there are no nodes are no edges in the graph, return the empty graph.
409 if n == 0 or max(w) == 0:
410 return G
411
412 rho = 1 / sum(w)
413 # Sort the weights in decreasing order. The original order of the
414 # weights dictates the order of the (integer) node labels, so we
415 # need to remember the permutation applied in the sorting.
416 order = sorted(enumerate(w), key=itemgetter(1), reverse=True)
417 mapping = {c: u for c, (u, v) in enumerate(order)}
418 seq = [v for u, v in order]
419 last = n
420 if not selfloops:
421 last -= 1
422 for u in range(last):
423 v = u
424 if not selfloops:
425 v += 1
426 factor = seq[u] * rho
427 p = min(seq[v] * factor, 1)
428 while v < n and p > 0:
429 if p != 1:
430 r = seed.random()
431 v += math.floor(math.log(r, 1 - p))
432 if v < n:
433 q = min(seq[v] * factor, 1)
434 if seed.random() < q / p:
435 G.add_edge(mapping[u], mapping[v])
436 v += 1
437 p = q
438 return G
439
440
441@nx._dispatchable(graphs=None, returns_graph=True)
442def havel_hakimi_graph(deg_sequence, create_using=None):
443 """Returns a simple graph with given degree sequence constructed
444 using the Havel-Hakimi algorithm.
445
446 Parameters
447 ----------
448 deg_sequence: list of integers
449 Each integer corresponds to the degree of a node (need not be sorted).
450 create_using : NetworkX graph constructor, optional (default=nx.Graph)
451 Graph type to create. If graph instance, then cleared before populated.
452 Directed graphs are not allowed.
453
454 Raises
455 ------
456 NetworkXException
457 For a non-graphical degree sequence (i.e. one
458 not realizable by some simple graph).
459
460 Notes
461 -----
462 The Havel-Hakimi algorithm constructs a simple graph by
463 successively connecting the node of highest degree to other nodes
464 of highest degree, resorting remaining nodes by degree, and
465 repeating the process. The resulting graph has a high
466 degree-associativity. Nodes are labeled 1,.., len(deg_sequence),
467 corresponding to their position in deg_sequence.
468
469 The basic algorithm is from Hakimi [1]_ and was generalized by
470 Kleitman and Wang [2]_.
471
472 References
473 ----------
474 .. [1] Hakimi S., On Realizability of a Set of Integers as
475 Degrees of the Vertices of a Linear Graph. I,
476 Journal of SIAM, 10(3), pp. 496-506 (1962)
477 .. [2] Kleitman D.J. and Wang D.L.
478 Algorithms for Constructing Graphs and Digraphs with Given Valences
479 and Factors Discrete Mathematics, 6(1), pp. 79-88 (1973)
480 """
481 if not nx.is_graphical(deg_sequence):
482 raise nx.NetworkXError("Invalid degree sequence")
483
484 p = len(deg_sequence)
485 G = nx.empty_graph(p, create_using)
486 if G.is_directed():
487 raise nx.NetworkXError("Directed graphs are not supported")
488 num_degs = [[] for i in range(p)]
489 dmax, dsum, n = 0, 0, 0
490 for d in deg_sequence:
491 # Process only the non-zero integers
492 if d > 0:
493 num_degs[d].append(n)
494 dmax, dsum, n = max(dmax, d), dsum + d, n + 1
495 # Return graph if no edges
496 if n == 0:
497 return G
498
499 modstubs = [(0, 0)] * (dmax + 1)
500 # Successively reduce degree sequence by removing the maximum degree
501 while n > 0:
502 # Retrieve the maximum degree in the sequence
503 while len(num_degs[dmax]) == 0:
504 dmax -= 1
505 # If there are not enough stubs to connect to, then the sequence is
506 # not graphical
507 if dmax > n - 1:
508 raise nx.NetworkXError("Non-graphical integer sequence")
509
510 # Remove largest stub in list
511 source = num_degs[dmax].pop()
512 n -= 1
513 # Reduce the next dmax largest stubs
514 mslen = 0
515 k = dmax
516 for i in range(dmax):
517 while len(num_degs[k]) == 0:
518 k -= 1
519 target = num_degs[k].pop()
520 G.add_edge(source, target)
521 n -= 1
522 if k > 1:
523 modstubs[mslen] = (k - 1, target)
524 mslen += 1
525 # Add back to the list any nonzero stubs that were removed
526 for i in range(mslen):
527 (stubval, stubtarget) = modstubs[i]
528 num_degs[stubval].append(stubtarget)
529 n += 1
530
531 return G
532
533
534@nx._dispatchable(graphs=None, returns_graph=True)
535def directed_havel_hakimi_graph(in_deg_sequence, out_deg_sequence, create_using=None):
536 """Returns a directed graph with the given degree sequences.
537
538 Parameters
539 ----------
540 in_deg_sequence : list of integers
541 Each list entry corresponds to the in-degree of a node.
542 out_deg_sequence : list of integers
543 Each list entry corresponds to the out-degree of a node.
544 create_using : NetworkX graph constructor, optional (default DiGraph)
545 Graph type to create. If graph instance, then cleared before populated.
546
547 Returns
548 -------
549 G : DiGraph
550 A graph with the specified degree sequences.
551 Nodes are labeled starting at 0 with an index
552 corresponding to the position in deg_sequence
553
554 Raises
555 ------
556 NetworkXError
557 If the degree sequences are not digraphical.
558
559 See Also
560 --------
561 configuration_model
562
563 Notes
564 -----
565 Algorithm as described by Kleitman and Wang [1]_.
566
567 References
568 ----------
569 .. [1] D.J. Kleitman and D.L. Wang
570 Algorithms for Constructing Graphs and Digraphs with Given Valences
571 and Factors Discrete Mathematics, 6(1), pp. 79-88 (1973)
572 """
573 in_deg_sequence = nx.utils.make_list_of_ints(in_deg_sequence)
574 out_deg_sequence = nx.utils.make_list_of_ints(out_deg_sequence)
575
576 # Process the sequences and form two heaps to store degree pairs with
577 # either zero or nonzero out degrees
578 sumin, sumout = 0, 0
579 nin, nout = len(in_deg_sequence), len(out_deg_sequence)
580 maxn = max(nin, nout)
581 G = nx.empty_graph(maxn, create_using, default=nx.DiGraph)
582 if maxn == 0:
583 return G
584 maxin = 0
585 stubheap, zeroheap = [], []
586 for n in range(maxn):
587 in_deg, out_deg = 0, 0
588 if n < nout:
589 out_deg = out_deg_sequence[n]
590 if n < nin:
591 in_deg = in_deg_sequence[n]
592 if in_deg < 0 or out_deg < 0:
593 raise nx.NetworkXError(
594 "Invalid degree sequences. Sequence values must be positive."
595 )
596 sumin, sumout, maxin = sumin + in_deg, sumout + out_deg, max(maxin, in_deg)
597 if in_deg > 0:
598 stubheap.append((-1 * out_deg, -1 * in_deg, n))
599 elif out_deg > 0:
600 zeroheap.append((-1 * out_deg, n))
601 if sumin != sumout:
602 raise nx.NetworkXError(
603 "Invalid degree sequences. Sequences must have equal sums."
604 )
605 heapq.heapify(stubheap)
606 heapq.heapify(zeroheap)
607
608 modstubs = [(0, 0, 0)] * (maxin + 1)
609 # Successively reduce degree sequence by removing the maximum
610 while stubheap:
611 # Remove first value in the sequence with a non-zero in degree
612 (freeout, freein, target) = heapq.heappop(stubheap)
613 freein *= -1
614 if freein > len(stubheap) + len(zeroheap):
615 raise nx.NetworkXError("Non-digraphical integer sequence")
616
617 # Attach arcs from the nodes with the most stubs
618 mslen = 0
619 for i in range(freein):
620 if zeroheap and (not stubheap or stubheap[0][0] > zeroheap[0][0]):
621 (stubout, stubsource) = heapq.heappop(zeroheap)
622 stubin = 0
623 else:
624 (stubout, stubin, stubsource) = heapq.heappop(stubheap)
625 if stubout == 0:
626 raise nx.NetworkXError("Non-digraphical integer sequence")
627 G.add_edge(stubsource, target)
628 # Check if source is now totally connected
629 if stubout + 1 < 0 or stubin < 0:
630 modstubs[mslen] = (stubout + 1, stubin, stubsource)
631 mslen += 1
632
633 # Add the nodes back to the heaps that still have available stubs
634 for i in range(mslen):
635 stub = modstubs[i]
636 if stub[1] < 0:
637 heapq.heappush(stubheap, stub)
638 else:
639 heapq.heappush(zeroheap, (stub[0], stub[2]))
640 if freeout < 0:
641 heapq.heappush(zeroheap, (freeout, target))
642
643 return G
644
645
646@nx._dispatchable(graphs=None, returns_graph=True)
647def degree_sequence_tree(deg_sequence, create_using=None):
648 """Return a tree with the given degree sequence.
649
650 Two conditions must be met for a degree sequence to be valid for a tree:
651
652 1. The number of nodes must be one more than the number of edges.
653 2. The degree sequence must be trivial or have only strictly positive
654 node degrees.
655
656 Parameters
657 ----------
658 degree_sequence : iterable
659 Iterable of node degrees.
660
661 create_using : NetworkX graph constructor, optional (default=nx.Graph)
662 Graph type to create. If graph instance, then cleared before populated.
663
664 Returns
665 -------
666 networkx.Graph
667 A tree with the given degree sequence.
668
669 Raises
670 ------
671 NetworkXError
672 If the degree sequence is not valid for a tree.
673
674 If `create_using` is directed.
675
676 See Also
677 --------
678 random_degree_sequence_graph
679 """
680 deg_sequence = list(deg_sequence)
681 valid, reason = nx.utils.is_valid_tree_degree_sequence(deg_sequence)
682 if not valid:
683 raise nx.NetworkXError(reason)
684
685 G = nx.empty_graph(0, create_using)
686 if G.is_directed():
687 raise nx.NetworkXError("Directed Graph not supported")
688
689 if deg_sequence == [0]:
690 G.add_node(0)
691 return G
692
693 # Sort all degrees greater than 1 in decreasing order.
694 #
695 # TODO Does this need to be sorted in reverse order?
696 deg = sorted((s for s in deg_sequence if s > 1), reverse=True)
697
698 # make path graph as backbone
699 n = len(deg) + 2
700 nx.add_path(G, range(n))
701 last = n
702
703 # add the leaves
704 for source in range(1, n - 1):
705 nedges = deg.pop() - 2
706 G.add_edges_from((source, target) for target in range(last, last + nedges))
707 last += nedges
708 return G
709
710
711@py_random_state(1)
712@nx._dispatchable(graphs=None, returns_graph=True)
713def random_degree_sequence_graph(sequence, seed=None, tries=10):
714 r"""Returns a simple random graph with the given degree sequence.
715
716 If the maximum degree $d_m$ in the sequence is $O(m^{1/4})$ then the
717 algorithm produces almost uniform random graphs in $O(m d_m)$ time
718 where $m$ is the number of edges.
719
720 Parameters
721 ----------
722 sequence : list of integers
723 Sequence of degrees
724 seed : integer, random_state, or None (default)
725 Indicator of random number generation state.
726 See :ref:`Randomness<randomness>`.
727 tries : int, optional
728 Maximum number of tries to create a graph
729
730 Returns
731 -------
732 G : Graph
733 A graph with the specified degree sequence.
734 Nodes are labeled starting at 0 with an index
735 corresponding to the position in the sequence.
736
737 Raises
738 ------
739 NetworkXUnfeasible
740 If the degree sequence is not graphical.
741 NetworkXError
742 If a graph is not produced in specified number of tries
743
744 See Also
745 --------
746 is_graphical, configuration_model
747
748 Notes
749 -----
750 The generator algorithm [1]_ is not guaranteed to produce a graph.
751
752 References
753 ----------
754 .. [1] Moshen Bayati, Jeong Han Kim, and Amin Saberi,
755 A sequential algorithm for generating random graphs.
756 Algorithmica, Volume 58, Number 4, 860-910,
757 DOI: 10.1007/s00453-009-9340-1
758
759 Examples
760 --------
761 >>> sequence = [1, 2, 2, 3]
762 >>> G = nx.random_degree_sequence_graph(sequence, seed=42)
763 >>> sorted(d for n, d in G.degree())
764 [1, 2, 2, 3]
765 """
766 DSRG = DegreeSequenceRandomGraph(sequence, seed)
767 for try_n in range(tries):
768 try:
769 return DSRG.generate()
770 except nx.NetworkXUnfeasible:
771 pass
772 raise nx.NetworkXError(f"failed to generate graph in {tries} tries")
773
774
775class DegreeSequenceRandomGraph:
776 # class to generate random graphs with a given degree sequence
777 # use random_degree_sequence_graph()
778 def __init__(self, degree, rng):
779 self.rng = rng
780 self.degree = list(degree)
781 if not nx.is_graphical(self.degree):
782 raise nx.NetworkXUnfeasible("degree sequence is not graphical")
783 # node labels are integers 0,...,n-1
784 self.m = sum(self.degree) / 2.0 # number of edges
785 try:
786 self.dmax = max(self.degree) # maximum degree
787 except ValueError:
788 self.dmax = 0
789
790 def generate(self):
791 # remaining_degree is mapping from int->remaining degree
792 self.remaining_degree = dict(enumerate(self.degree))
793 # add all nodes to make sure we get isolated nodes
794 self.graph = nx.Graph()
795 self.graph.add_nodes_from(self.remaining_degree)
796 # remove zero degree nodes
797 for n, d in list(self.remaining_degree.items()):
798 if d == 0:
799 del self.remaining_degree[n]
800 if len(self.remaining_degree) > 0:
801 # build graph in three phases according to how many unmatched edges
802 self.phase1()
803 self.phase2()
804 self.phase3()
805 return self.graph
806
807 def update_remaining(self, u, v, aux_graph=None):
808 # decrement remaining nodes, modify auxiliary graph if in phase3
809 if aux_graph is not None:
810 # remove edges from auxiliary graph
811 aux_graph.remove_edge(u, v)
812 if self.remaining_degree[u] == 1:
813 del self.remaining_degree[u]
814 if aux_graph is not None:
815 aux_graph.remove_node(u)
816 else:
817 self.remaining_degree[u] -= 1
818 if self.remaining_degree[v] == 1:
819 del self.remaining_degree[v]
820 if aux_graph is not None:
821 aux_graph.remove_node(v)
822 else:
823 self.remaining_degree[v] -= 1
824
825 def p(self, u, v):
826 # degree probability
827 return 1 - self.degree[u] * self.degree[v] / (4.0 * self.m)
828
829 def q(self, u, v):
830 # remaining degree probability
831 norm = max(self.remaining_degree.values()) ** 2
832 return self.remaining_degree[u] * self.remaining_degree[v] / norm
833
834 def suitable_edge(self):
835 """Returns True if and only if an arbitrary remaining node can
836 potentially be joined with some other remaining node.
837
838 """
839 nodes = iter(self.remaining_degree)
840 u = next(nodes)
841 return any(v not in self.graph[u] for v in nodes)
842
843 def phase1(self):
844 # choose node pairs from (degree) weighted distribution
845 rem_deg = self.remaining_degree
846 while sum(rem_deg.values()) >= 2 * self.dmax**2:
847 u, v = sorted(random_weighted_sample(rem_deg, 2, self.rng))
848 if self.graph.has_edge(u, v):
849 continue
850 if self.rng.random() < self.p(u, v): # accept edge
851 self.graph.add_edge(u, v)
852 self.update_remaining(u, v)
853
854 def phase2(self):
855 # choose remaining nodes uniformly at random and use rejection sampling
856 remaining_deg = self.remaining_degree
857 rng = self.rng
858 while len(remaining_deg) >= 2 * self.dmax:
859 while True:
860 u, v = sorted(rng.sample(list(remaining_deg.keys()), 2))
861 if self.graph.has_edge(u, v):
862 continue
863 if rng.random() < self.q(u, v):
864 break
865 if rng.random() < self.p(u, v): # accept edge
866 self.graph.add_edge(u, v)
867 self.update_remaining(u, v)
868
869 def phase3(self):
870 # build potential remaining edges and choose with rejection sampling
871 potential_edges = combinations(self.remaining_degree, 2)
872 # build auxiliary graph of potential edges not already in graph
873 H = nx.Graph(
874 [(u, v) for (u, v) in potential_edges if not self.graph.has_edge(u, v)]
875 )
876 rng = self.rng
877 while self.remaining_degree:
878 if not self.suitable_edge():
879 raise nx.NetworkXUnfeasible("no suitable edges left")
880 while True:
881 u, v = sorted(rng.choice(list(H.edges())))
882 if rng.random() < self.q(u, v):
883 break
884 if rng.random() < self.p(u, v): # accept edge
885 self.graph.add_edge(u, v)
886 self.update_remaining(u, v, aux_graph=H)