123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861 |
- """Generate graphs with a given degree sequence or expected degree sequence.
- """
- import heapq
- import math
- from itertools import chain, combinations, zip_longest
- from operator import itemgetter
- import networkx as nx
- from networkx.utils import py_random_state, random_weighted_sample
- __all__ = [
- "configuration_model",
- "directed_configuration_model",
- "expected_degree_graph",
- "havel_hakimi_graph",
- "directed_havel_hakimi_graph",
- "degree_sequence_tree",
- "random_degree_sequence_graph",
- ]
- chaini = chain.from_iterable
- def _to_stublist(degree_sequence):
- """Returns a list of degree-repeated node numbers.
- ``degree_sequence`` is a list of nonnegative integers representing
- the degrees of nodes in a graph.
- This function returns a list of node numbers with multiplicities
- according to the given degree sequence. For example, if the first
- element of ``degree_sequence`` is ``3``, then the first node number,
- ``0``, will appear at the head of the returned list three times. The
- node numbers are assumed to be the numbers zero through
- ``len(degree_sequence) - 1``.
- Examples
- --------
- >>> degree_sequence = [1, 2, 3]
- >>> _to_stublist(degree_sequence)
- [0, 1, 1, 2, 2, 2]
- If a zero appears in the sequence, that means the node exists but
- has degree zero, so that number will be skipped in the returned
- list::
- >>> degree_sequence = [2, 0, 1]
- >>> _to_stublist(degree_sequence)
- [0, 0, 2]
- """
- return list(chaini([n] * d for n, d in enumerate(degree_sequence)))
- def _configuration_model(
- deg_sequence, create_using, directed=False, in_deg_sequence=None, seed=None
- ):
- """Helper function for generating either undirected or directed
- configuration model graphs.
- ``deg_sequence`` is a list of nonnegative integers representing the
- degree of the node whose label is the index of the list element.
- ``create_using`` see :func:`~networkx.empty_graph`.
- ``directed`` and ``in_deg_sequence`` are required if you want the
- returned graph to be generated using the directed configuration
- model algorithm. If ``directed`` is ``False``, then ``deg_sequence``
- is interpreted as the degree sequence of an undirected graph and
- ``in_deg_sequence`` is ignored. Otherwise, if ``directed`` is
- ``True``, then ``deg_sequence`` is interpreted as the out-degree
- sequence and ``in_deg_sequence`` as the in-degree sequence of a
- directed graph.
- .. note::
- ``deg_sequence`` and ``in_deg_sequence`` need not be the same
- length.
- ``seed`` is a random.Random or numpy.random.RandomState instance
- This function returns a graph, directed if and only if ``directed``
- is ``True``, generated according to the configuration model
- algorithm. For more information on the algorithm, see the
- :func:`configuration_model` or :func:`directed_configuration_model`
- functions.
- """
- n = len(deg_sequence)
- G = nx.empty_graph(n, create_using)
- # If empty, return the null graph immediately.
- if n == 0:
- return G
- # Build a list of available degree-repeated nodes. For example,
- # for degree sequence [3, 2, 1, 1, 1], the "stub list" is
- # initially [0, 0, 0, 1, 1, 2, 3, 4], that is, node 0 has degree
- # 3 and thus is repeated 3 times, etc.
- #
- # Also, shuffle the stub list in order to get a random sequence of
- # node pairs.
- if directed:
- pairs = zip_longest(deg_sequence, in_deg_sequence, fillvalue=0)
- # Unzip the list of pairs into a pair of lists.
- out_deg, in_deg = zip(*pairs)
- out_stublist = _to_stublist(out_deg)
- in_stublist = _to_stublist(in_deg)
- seed.shuffle(out_stublist)
- seed.shuffle(in_stublist)
- else:
- stublist = _to_stublist(deg_sequence)
- # Choose a random balanced bipartition of the stublist, which
- # gives a random pairing of nodes. In this implementation, we
- # shuffle the list and then split it in half.
- n = len(stublist)
- half = n // 2
- seed.shuffle(stublist)
- out_stublist, in_stublist = stublist[:half], stublist[half:]
- G.add_edges_from(zip(out_stublist, in_stublist))
- return G
- @py_random_state(2)
- def configuration_model(deg_sequence, create_using=None, seed=None):
- """Returns a random graph with the given degree sequence.
- The configuration model generates a random pseudograph (graph with
- parallel edges and self loops) by randomly assigning edges to
- match the given degree sequence.
- Parameters
- ----------
- deg_sequence : list of nonnegative integers
- Each list entry corresponds to the degree of a node.
- create_using : NetworkX graph constructor, optional (default MultiGraph)
- Graph type to create. If graph instance, then cleared before populated.
- seed : integer, random_state, or None (default)
- Indicator of random number generation state.
- See :ref:`Randomness<randomness>`.
- Returns
- -------
- G : MultiGraph
- A graph with the specified degree sequence.
- Nodes are labeled starting at 0 with an index
- corresponding to the position in deg_sequence.
- Raises
- ------
- NetworkXError
- If the degree sequence does not have an even sum.
- See Also
- --------
- is_graphical
- Notes
- -----
- As described by Newman [1]_.
- A non-graphical degree sequence (not realizable by some simple
- graph) is allowed since this function returns graphs with self
- loops and parallel edges. An exception is raised if the degree
- sequence does not have an even sum.
- This configuration model construction process can lead to
- duplicate edges and loops. You can remove the self-loops and
- parallel edges (see below) which will likely result in a graph
- that doesn't have the exact degree sequence specified.
- The density of self-loops and parallel edges tends to decrease as
- the number of nodes increases. However, typically the number of
- self-loops will approach a Poisson distribution with a nonzero mean,
- and similarly for the number of parallel edges. Consider a node
- with *k* stubs. The probability of being joined to another stub of
- the same node is basically (*k* - *1*) / *N*, where *k* is the
- degree and *N* is the number of nodes. So the probability of a
- self-loop scales like *c* / *N* for some constant *c*. As *N* grows,
- this means we expect *c* self-loops. Similarly for parallel edges.
- References
- ----------
- .. [1] M.E.J. Newman, "The structure and function of complex networks",
- SIAM REVIEW 45-2, pp 167-256, 2003.
- Examples
- --------
- You can create a degree sequence following a particular distribution
- by using the one of the distribution functions in
- :mod:`~networkx.utils.random_sequence` (or one of your own). For
- example, to create an undirected multigraph on one hundred nodes
- with degree sequence chosen from the power law distribution:
- >>> sequence = nx.random_powerlaw_tree_sequence(100, tries=5000)
- >>> G = nx.configuration_model(sequence)
- >>> len(G)
- 100
- >>> actual_degrees = [d for v, d in G.degree()]
- >>> actual_degrees == sequence
- True
- The returned graph is a multigraph, which may have parallel
- edges. To remove any parallel edges from the returned graph:
- >>> G = nx.Graph(G)
- Similarly, to remove self-loops:
- >>> G.remove_edges_from(nx.selfloop_edges(G))
- """
- if sum(deg_sequence) % 2 != 0:
- msg = "Invalid degree sequence: sum of degrees must be even, not odd"
- raise nx.NetworkXError(msg)
- G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
- if G.is_directed():
- raise nx.NetworkXNotImplemented("not implemented for directed graphs")
- G = _configuration_model(deg_sequence, G, seed=seed)
- return G
- @py_random_state(3)
- def directed_configuration_model(
- in_degree_sequence, out_degree_sequence, create_using=None, seed=None
- ):
- """Returns a directed_random graph with the given degree sequences.
- The configuration model generates a random directed pseudograph
- (graph with parallel edges and self loops) by randomly assigning
- edges to match the given degree sequences.
- Parameters
- ----------
- in_degree_sequence : list of nonnegative integers
- Each list entry corresponds to the in-degree of a node.
- out_degree_sequence : list of nonnegative integers
- Each list entry corresponds to the out-degree of a node.
- create_using : NetworkX graph constructor, optional (default MultiDiGraph)
- Graph type to create. If graph instance, then cleared before populated.
- seed : integer, random_state, or None (default)
- Indicator of random number generation state.
- See :ref:`Randomness<randomness>`.
- Returns
- -------
- G : MultiDiGraph
- A graph with the specified degree sequences.
- Nodes are labeled starting at 0 with an index
- corresponding to the position in deg_sequence.
- Raises
- ------
- NetworkXError
- If the degree sequences do not have the same sum.
- See Also
- --------
- configuration_model
- Notes
- -----
- Algorithm as described by Newman [1]_.
- A non-graphical degree sequence (not realizable by some simple
- graph) is allowed since this function returns graphs with self
- loops and parallel edges. An exception is raised if the degree
- sequences does not have the same sum.
- This configuration model construction process can lead to
- duplicate edges and loops. You can remove the self-loops and
- parallel edges (see below) which will likely result in a graph
- that doesn't have the exact degree sequence specified. This
- "finite-size effect" decreases as the size of the graph increases.
- References
- ----------
- .. [1] Newman, M. E. J. and Strogatz, S. H. and Watts, D. J.
- Random graphs with arbitrary degree distributions and their applications
- Phys. Rev. E, 64, 026118 (2001)
- Examples
- --------
- One can modify the in- and out-degree sequences from an existing
- directed graph in order to create a new directed graph. For example,
- here we modify the directed path graph:
- >>> D = nx.DiGraph([(0, 1), (1, 2), (2, 3)])
- >>> din = list(d for n, d in D.in_degree())
- >>> dout = list(d for n, d in D.out_degree())
- >>> din.append(1)
- >>> dout[0] = 2
- >>> # We now expect an edge from node 0 to a new node, node 3.
- ... D = nx.directed_configuration_model(din, dout)
- The returned graph is a directed multigraph, which may have parallel
- edges. To remove any parallel edges from the returned graph:
- >>> D = nx.DiGraph(D)
- Similarly, to remove self-loops:
- >>> D.remove_edges_from(nx.selfloop_edges(D))
- """
- if sum(in_degree_sequence) != sum(out_degree_sequence):
- msg = "Invalid degree sequences: sequences must have equal sums"
- raise nx.NetworkXError(msg)
- if create_using is None:
- create_using = nx.MultiDiGraph
- G = _configuration_model(
- out_degree_sequence,
- create_using,
- directed=True,
- in_deg_sequence=in_degree_sequence,
- seed=seed,
- )
- name = "directed configuration_model {} nodes {} edges"
- return G
- @py_random_state(1)
- def expected_degree_graph(w, seed=None, selfloops=True):
- r"""Returns a random graph with given expected degrees.
- Given a sequence of expected degrees $W=(w_0,w_1,\ldots,w_{n-1})$
- of length $n$ this algorithm assigns an edge between node $u$ and
- node $v$ with probability
- .. math::
- p_{uv} = \frac{w_u w_v}{\sum_k w_k} .
- Parameters
- ----------
- w : list
- The list of expected degrees.
- selfloops: bool (default=True)
- Set to False to remove the possibility of self-loop edges.
- seed : integer, random_state, or None (default)
- Indicator of random number generation state.
- See :ref:`Randomness<randomness>`.
- Returns
- -------
- Graph
- Examples
- --------
- >>> z = [10 for i in range(100)]
- >>> G = nx.expected_degree_graph(z)
- Notes
- -----
- The nodes have integer labels corresponding to index of expected degrees
- input sequence.
- The complexity of this algorithm is $\mathcal{O}(n+m)$ where $n$ is the
- number of nodes and $m$ is the expected number of edges.
- The model in [1]_ includes the possibility of self-loop edges.
- Set selfloops=False to produce a graph without self loops.
- For finite graphs this model doesn't produce exactly the given
- expected degree sequence. Instead the expected degrees are as
- follows.
- For the case without self loops (selfloops=False),
- .. math::
- E[deg(u)] = \sum_{v \ne u} p_{uv}
- = w_u \left( 1 - \frac{w_u}{\sum_k w_k} \right) .
- NetworkX uses the standard convention that a self-loop edge counts 2
- in the degree of a node, so with self loops (selfloops=True),
- .. math::
- E[deg(u)] = \sum_{v \ne u} p_{uv} + 2 p_{uu}
- = w_u \left( 1 + \frac{w_u}{\sum_k w_k} \right) .
- References
- ----------
- .. [1] Fan Chung and L. Lu, Connected components in random graphs with
- given expected degree sequences, Ann. Combinatorics, 6,
- pp. 125-145, 2002.
- .. [2] Joel Miller and Aric Hagberg,
- Efficient generation of networks with given expected degrees,
- in Algorithms and Models for the Web-Graph (WAW 2011),
- Alan Frieze, Paul Horn, and Paweł Prałat (Eds), LNCS 6732,
- pp. 115-126, 2011.
- """
- n = len(w)
- G = nx.empty_graph(n)
- # If there are no nodes are no edges in the graph, return the empty graph.
- if n == 0 or max(w) == 0:
- return G
- rho = 1 / sum(w)
- # Sort the weights in decreasing order. The original order of the
- # weights dictates the order of the (integer) node labels, so we
- # need to remember the permutation applied in the sorting.
- order = sorted(enumerate(w), key=itemgetter(1), reverse=True)
- mapping = {c: u for c, (u, v) in enumerate(order)}
- seq = [v for u, v in order]
- last = n
- if not selfloops:
- last -= 1
- for u in range(last):
- v = u
- if not selfloops:
- v += 1
- factor = seq[u] * rho
- p = min(seq[v] * factor, 1)
- while v < n and p > 0:
- if p != 1:
- r = seed.random()
- v += math.floor(math.log(r, 1 - p))
- if v < n:
- q = min(seq[v] * factor, 1)
- if seed.random() < q / p:
- G.add_edge(mapping[u], mapping[v])
- v += 1
- p = q
- return G
- def havel_hakimi_graph(deg_sequence, create_using=None):
- """Returns a simple graph with given degree sequence constructed
- using the Havel-Hakimi algorithm.
- Parameters
- ----------
- deg_sequence: list of integers
- Each integer corresponds to the degree of a node (need not be sorted).
- create_using : NetworkX graph constructor, optional (default=nx.Graph)
- Graph type to create. If graph instance, then cleared before populated.
- Directed graphs are not allowed.
- Raises
- ------
- NetworkXException
- For a non-graphical degree sequence (i.e. one
- not realizable by some simple graph).
- Notes
- -----
- The Havel-Hakimi algorithm constructs a simple graph by
- successively connecting the node of highest degree to other nodes
- of highest degree, resorting remaining nodes by degree, and
- repeating the process. The resulting graph has a high
- degree-associativity. Nodes are labeled 1,.., len(deg_sequence),
- corresponding to their position in deg_sequence.
- The basic algorithm is from Hakimi [1]_ and was generalized by
- Kleitman and Wang [2]_.
- References
- ----------
- .. [1] Hakimi S., On Realizability of a Set of Integers as
- Degrees of the Vertices of a Linear Graph. I,
- Journal of SIAM, 10(3), pp. 496-506 (1962)
- .. [2] Kleitman D.J. and Wang D.L.
- Algorithms for Constructing Graphs and Digraphs with Given Valences
- and Factors Discrete Mathematics, 6(1), pp. 79-88 (1973)
- """
- if not nx.is_graphical(deg_sequence):
- raise nx.NetworkXError("Invalid degree sequence")
- p = len(deg_sequence)
- G = nx.empty_graph(p, create_using)
- if G.is_directed():
- raise nx.NetworkXError("Directed graphs are not supported")
- num_degs = [[] for i in range(p)]
- dmax, dsum, n = 0, 0, 0
- for d in deg_sequence:
- # Process only the non-zero integers
- if d > 0:
- num_degs[d].append(n)
- dmax, dsum, n = max(dmax, d), dsum + d, n + 1
- # Return graph if no edges
- if n == 0:
- return G
- modstubs = [(0, 0)] * (dmax + 1)
- # Successively reduce degree sequence by removing the maximum degree
- while n > 0:
- # Retrieve the maximum degree in the sequence
- while len(num_degs[dmax]) == 0:
- dmax -= 1
- # If there are not enough stubs to connect to, then the sequence is
- # not graphical
- if dmax > n - 1:
- raise nx.NetworkXError("Non-graphical integer sequence")
- # Remove largest stub in list
- source = num_degs[dmax].pop()
- n -= 1
- # Reduce the next dmax largest stubs
- mslen = 0
- k = dmax
- for i in range(dmax):
- while len(num_degs[k]) == 0:
- k -= 1
- target = num_degs[k].pop()
- G.add_edge(source, target)
- n -= 1
- if k > 1:
- modstubs[mslen] = (k - 1, target)
- mslen += 1
- # Add back to the list any nonzero stubs that were removed
- for i in range(mslen):
- (stubval, stubtarget) = modstubs[i]
- num_degs[stubval].append(stubtarget)
- n += 1
- return G
- def directed_havel_hakimi_graph(in_deg_sequence, out_deg_sequence, create_using=None):
- """Returns a directed graph with the given degree sequences.
- Parameters
- ----------
- in_deg_sequence : list of integers
- Each list entry corresponds to the in-degree of a node.
- out_deg_sequence : list of integers
- Each list entry corresponds to the out-degree of a node.
- create_using : NetworkX graph constructor, optional (default DiGraph)
- Graph type to create. If graph instance, then cleared before populated.
- Returns
- -------
- G : DiGraph
- A graph with the specified degree sequences.
- Nodes are labeled starting at 0 with an index
- corresponding to the position in deg_sequence
- Raises
- ------
- NetworkXError
- If the degree sequences are not digraphical.
- See Also
- --------
- configuration_model
- Notes
- -----
- Algorithm as described by Kleitman and Wang [1]_.
- References
- ----------
- .. [1] D.J. Kleitman and D.L. Wang
- Algorithms for Constructing Graphs and Digraphs with Given Valences
- and Factors Discrete Mathematics, 6(1), pp. 79-88 (1973)
- """
- in_deg_sequence = nx.utils.make_list_of_ints(in_deg_sequence)
- out_deg_sequence = nx.utils.make_list_of_ints(out_deg_sequence)
- # Process the sequences and form two heaps to store degree pairs with
- # either zero or nonzero out degrees
- sumin, sumout = 0, 0
- nin, nout = len(in_deg_sequence), len(out_deg_sequence)
- maxn = max(nin, nout)
- G = nx.empty_graph(maxn, create_using, default=nx.DiGraph)
- if maxn == 0:
- return G
- maxin = 0
- stubheap, zeroheap = [], []
- for n in range(maxn):
- in_deg, out_deg = 0, 0
- if n < nout:
- out_deg = out_deg_sequence[n]
- if n < nin:
- in_deg = in_deg_sequence[n]
- if in_deg < 0 or out_deg < 0:
- raise nx.NetworkXError(
- "Invalid degree sequences. Sequence values must be positive."
- )
- sumin, sumout, maxin = sumin + in_deg, sumout + out_deg, max(maxin, in_deg)
- if in_deg > 0:
- stubheap.append((-1 * out_deg, -1 * in_deg, n))
- elif out_deg > 0:
- zeroheap.append((-1 * out_deg, n))
- if sumin != sumout:
- raise nx.NetworkXError(
- "Invalid degree sequences. Sequences must have equal sums."
- )
- heapq.heapify(stubheap)
- heapq.heapify(zeroheap)
- modstubs = [(0, 0, 0)] * (maxin + 1)
- # Successively reduce degree sequence by removing the maximum
- while stubheap:
- # Remove first value in the sequence with a non-zero in degree
- (freeout, freein, target) = heapq.heappop(stubheap)
- freein *= -1
- if freein > len(stubheap) + len(zeroheap):
- raise nx.NetworkXError("Non-digraphical integer sequence")
- # Attach arcs from the nodes with the most stubs
- mslen = 0
- for i in range(freein):
- if zeroheap and (not stubheap or stubheap[0][0] > zeroheap[0][0]):
- (stubout, stubsource) = heapq.heappop(zeroheap)
- stubin = 0
- else:
- (stubout, stubin, stubsource) = heapq.heappop(stubheap)
- if stubout == 0:
- raise nx.NetworkXError("Non-digraphical integer sequence")
- G.add_edge(stubsource, target)
- # Check if source is now totally connected
- if stubout + 1 < 0 or stubin < 0:
- modstubs[mslen] = (stubout + 1, stubin, stubsource)
- mslen += 1
- # Add the nodes back to the heaps that still have available stubs
- for i in range(mslen):
- stub = modstubs[i]
- if stub[1] < 0:
- heapq.heappush(stubheap, stub)
- else:
- heapq.heappush(zeroheap, (stub[0], stub[2]))
- if freeout < 0:
- heapq.heappush(zeroheap, (freeout, target))
- return G
- def degree_sequence_tree(deg_sequence, create_using=None):
- """Make a tree for the given degree sequence.
- A tree has #nodes-#edges=1 so
- the degree sequence must have
- len(deg_sequence)-sum(deg_sequence)/2=1
- """
- # The sum of the degree sequence must be even (for any undirected graph).
- degree_sum = sum(deg_sequence)
- if degree_sum % 2 != 0:
- msg = "Invalid degree sequence: sum of degrees must be even, not odd"
- raise nx.NetworkXError(msg)
- if len(deg_sequence) - degree_sum // 2 != 1:
- msg = (
- "Invalid degree sequence: tree must have number of nodes equal"
- " to one less than the number of edges"
- )
- raise nx.NetworkXError(msg)
- G = nx.empty_graph(0, create_using)
- if G.is_directed():
- raise nx.NetworkXError("Directed Graph not supported")
- # Sort all degrees greater than 1 in decreasing order.
- #
- # TODO Does this need to be sorted in reverse order?
- deg = sorted((s for s in deg_sequence if s > 1), reverse=True)
- # make path graph as backbone
- n = len(deg) + 2
- nx.add_path(G, range(n))
- last = n
- # add the leaves
- for source in range(1, n - 1):
- nedges = deg.pop() - 2
- for target in range(last, last + nedges):
- G.add_edge(source, target)
- last += nedges
- # in case we added one too many
- if len(G) > len(deg_sequence):
- G.remove_node(0)
- return G
- @py_random_state(1)
- def random_degree_sequence_graph(sequence, seed=None, tries=10):
- r"""Returns a simple random graph with the given degree sequence.
- If the maximum degree $d_m$ in the sequence is $O(m^{1/4})$ then the
- algorithm produces almost uniform random graphs in $O(m d_m)$ time
- where $m$ is the number of edges.
- Parameters
- ----------
- sequence : list of integers
- Sequence of degrees
- seed : integer, random_state, or None (default)
- Indicator of random number generation state.
- See :ref:`Randomness<randomness>`.
- tries : int, optional
- Maximum number of tries to create a graph
- Returns
- -------
- G : Graph
- A graph with the specified degree sequence.
- Nodes are labeled starting at 0 with an index
- corresponding to the position in the sequence.
- Raises
- ------
- NetworkXUnfeasible
- If the degree sequence is not graphical.
- NetworkXError
- If a graph is not produced in specified number of tries
- See Also
- --------
- is_graphical, configuration_model
- Notes
- -----
- The generator algorithm [1]_ is not guaranteed to produce a graph.
- References
- ----------
- .. [1] Moshen Bayati, Jeong Han Kim, and Amin Saberi,
- A sequential algorithm for generating random graphs.
- Algorithmica, Volume 58, Number 4, 860-910,
- DOI: 10.1007/s00453-009-9340-1
- Examples
- --------
- >>> sequence = [1, 2, 2, 3]
- >>> G = nx.random_degree_sequence_graph(sequence, seed=42)
- >>> sorted(d for n, d in G.degree())
- [1, 2, 2, 3]
- """
- DSRG = DegreeSequenceRandomGraph(sequence, seed)
- for try_n in range(tries):
- try:
- return DSRG.generate()
- except nx.NetworkXUnfeasible:
- pass
- raise nx.NetworkXError(f"failed to generate graph in {tries} tries")
- class DegreeSequenceRandomGraph:
- # class to generate random graphs with a given degree sequence
- # use random_degree_sequence_graph()
- def __init__(self, degree, rng):
- if not nx.is_graphical(degree):
- raise nx.NetworkXUnfeasible("degree sequence is not graphical")
- self.rng = rng
- self.degree = list(degree)
- # node labels are integers 0,...,n-1
- self.m = sum(self.degree) / 2.0 # number of edges
- try:
- self.dmax = max(self.degree) # maximum degree
- except ValueError:
- self.dmax = 0
- def generate(self):
- # remaining_degree is mapping from int->remaining degree
- self.remaining_degree = dict(enumerate(self.degree))
- # add all nodes to make sure we get isolated nodes
- self.graph = nx.Graph()
- self.graph.add_nodes_from(self.remaining_degree)
- # remove zero degree nodes
- for n, d in list(self.remaining_degree.items()):
- if d == 0:
- del self.remaining_degree[n]
- if len(self.remaining_degree) > 0:
- # build graph in three phases according to how many unmatched edges
- self.phase1()
- self.phase2()
- self.phase3()
- return self.graph
- def update_remaining(self, u, v, aux_graph=None):
- # decrement remaining nodes, modify auxiliary graph if in phase3
- if aux_graph is not None:
- # remove edges from auxiliary graph
- aux_graph.remove_edge(u, v)
- if self.remaining_degree[u] == 1:
- del self.remaining_degree[u]
- if aux_graph is not None:
- aux_graph.remove_node(u)
- else:
- self.remaining_degree[u] -= 1
- if self.remaining_degree[v] == 1:
- del self.remaining_degree[v]
- if aux_graph is not None:
- aux_graph.remove_node(v)
- else:
- self.remaining_degree[v] -= 1
- def p(self, u, v):
- # degree probability
- return 1 - self.degree[u] * self.degree[v] / (4.0 * self.m)
- def q(self, u, v):
- # remaining degree probability
- norm = max(self.remaining_degree.values()) ** 2
- return self.remaining_degree[u] * self.remaining_degree[v] / norm
- def suitable_edge(self):
- """Returns True if and only if an arbitrary remaining node can
- potentially be joined with some other remaining node.
- """
- nodes = iter(self.remaining_degree)
- u = next(nodes)
- return any(v not in self.graph[u] for v in nodes)
- def phase1(self):
- # choose node pairs from (degree) weighted distribution
- rem_deg = self.remaining_degree
- while sum(rem_deg.values()) >= 2 * self.dmax**2:
- u, v = sorted(random_weighted_sample(rem_deg, 2, self.rng))
- if self.graph.has_edge(u, v):
- continue
- if self.rng.random() < self.p(u, v): # accept edge
- self.graph.add_edge(u, v)
- self.update_remaining(u, v)
- def phase2(self):
- # choose remaining nodes uniformly at random and use rejection sampling
- remaining_deg = self.remaining_degree
- rng = self.rng
- while len(remaining_deg) >= 2 * self.dmax:
- while True:
- u, v = sorted(rng.sample(list(remaining_deg.keys()), 2))
- if self.graph.has_edge(u, v):
- continue
- if rng.random() < self.q(u, v):
- break
- if rng.random() < self.p(u, v): # accept edge
- self.graph.add_edge(u, v)
- self.update_remaining(u, v)
- def phase3(self):
- # build potential remaining edges and choose with rejection sampling
- potential_edges = combinations(self.remaining_degree, 2)
- # build auxiliary graph of potential edges not already in graph
- H = nx.Graph(
- [(u, v) for (u, v) in potential_edges if not self.graph.has_edge(u, v)]
- )
- rng = self.rng
- while self.remaining_degree:
- if not self.suitable_edge():
- raise nx.NetworkXUnfeasible("no suitable edges left")
- while True:
- u, v = sorted(rng.choice(list(H.edges())))
- if rng.random() < self.q(u, v):
- break
- if rng.random() < self.p(u, v): # accept edge
- self.graph.add_edge(u, v)
- self.update_remaining(u, v, aux_graph=H)
|