| """ |
| Generators for some directed graphs, including growing network (GN) graphs and |
| scale-free graphs. |
| |
| """ |
|
|
| import numbers |
| from collections import Counter |
|
|
| import networkx as nx |
| from networkx.generators.classic import empty_graph |
| from networkx.utils import ( |
| discrete_sequence, |
| np_random_state, |
| py_random_state, |
| weighted_choice, |
| ) |
|
|
| __all__ = [ |
| "gn_graph", |
| "gnc_graph", |
| "gnr_graph", |
| "random_k_out_graph", |
| "scale_free_graph", |
| ] |
|
|
|
|
| @py_random_state(3) |
| @nx._dispatchable(graphs=None, returns_graph=True) |
| def gn_graph(n, kernel=None, create_using=None, seed=None): |
| """Returns the growing network (GN) digraph with `n` nodes. |
| |
| The GN graph is built by adding nodes one at a time with a link to one |
| previously added node. The target node for the link is chosen with |
| probability based on degree. The default attachment kernel is a linear |
| function of the degree of a node. |
| |
| The graph is always a (directed) tree. |
| |
| Parameters |
| ---------- |
| n : int |
| The number of nodes for the generated graph. |
| kernel : function |
| The attachment kernel. |
| create_using : NetworkX graph constructor, optional (default DiGraph) |
| 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>`. |
| |
| Examples |
| -------- |
| To create the undirected GN graph, use the :meth:`~DiGraph.to_directed` |
| method:: |
| |
| >>> D = nx.gn_graph(10) # the GN graph |
| >>> G = D.to_undirected() # the undirected version |
| |
| To specify an attachment kernel, use the `kernel` keyword argument:: |
| |
| >>> D = nx.gn_graph(10, kernel=lambda x: x**1.5) # A_k = k^1.5 |
| |
| References |
| ---------- |
| .. [1] P. L. Krapivsky and S. Redner, |
| Organization of Growing Random Networks, |
| Phys. Rev. E, 63, 066123, 2001. |
| """ |
| G = empty_graph(1, create_using, default=nx.DiGraph) |
| if not G.is_directed(): |
| raise nx.NetworkXError("create_using must indicate a Directed Graph") |
|
|
| if kernel is None: |
|
|
| def kernel(x): |
| return x |
|
|
| if n == 1: |
| return G |
|
|
| G.add_edge(1, 0) |
| ds = [1, 1] |
|
|
| for source in range(2, n): |
| |
| dist = [kernel(d) for d in ds] |
| |
| target = discrete_sequence(1, distribution=dist, seed=seed)[0] |
| G.add_edge(source, target) |
| ds.append(1) |
| ds[target] += 1 |
| return G |
|
|
|
|
| @py_random_state(3) |
| @nx._dispatchable(graphs=None, returns_graph=True) |
| def gnr_graph(n, p, create_using=None, seed=None): |
| """Returns the growing network with redirection (GNR) digraph with `n` |
| nodes and redirection probability `p`. |
| |
| The GNR graph is built by adding nodes one at a time with a link to one |
| previously added node. The previous target node is chosen uniformly at |
| random. With probability `p` the link is instead "redirected" to the |
| successor node of the target. |
| |
| The graph is always a (directed) tree. |
| |
| Parameters |
| ---------- |
| n : int |
| The number of nodes for the generated graph. |
| p : float |
| The redirection probability. |
| create_using : NetworkX graph constructor, optional (default DiGraph) |
| 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>`. |
| |
| Examples |
| -------- |
| To create the undirected GNR graph, use the :meth:`~DiGraph.to_directed` |
| method:: |
| |
| >>> D = nx.gnr_graph(10, 0.5) # the GNR graph |
| >>> G = D.to_undirected() # the undirected version |
| |
| References |
| ---------- |
| .. [1] P. L. Krapivsky and S. Redner, |
| Organization of Growing Random Networks, |
| Phys. Rev. E, 63, 066123, 2001. |
| """ |
| G = empty_graph(1, create_using, default=nx.DiGraph) |
| if not G.is_directed(): |
| raise nx.NetworkXError("create_using must indicate a Directed Graph") |
|
|
| if n == 1: |
| return G |
|
|
| for source in range(1, n): |
| target = seed.randrange(0, source) |
| if seed.random() < p and target != 0: |
| target = next(G.successors(target)) |
| G.add_edge(source, target) |
| return G |
|
|
|
|
| @py_random_state(2) |
| @nx._dispatchable(graphs=None, returns_graph=True) |
| def gnc_graph(n, create_using=None, seed=None): |
| """Returns the growing network with copying (GNC) digraph with `n` nodes. |
| |
| The GNC graph is built by adding nodes one at a time with a link to one |
| previously added node (chosen uniformly at random) and to all of that |
| node's successors. |
| |
| Parameters |
| ---------- |
| n : int |
| The number of nodes for the generated graph. |
| create_using : NetworkX graph constructor, optional (default DiGraph) |
| 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>`. |
| |
| References |
| ---------- |
| .. [1] P. L. Krapivsky and S. Redner, |
| Network Growth by Copying, |
| Phys. Rev. E, 71, 036118, 2005k.}, |
| """ |
| G = empty_graph(1, create_using, default=nx.DiGraph) |
| if not G.is_directed(): |
| raise nx.NetworkXError("create_using must indicate a Directed Graph") |
|
|
| if n == 1: |
| return G |
|
|
| for source in range(1, n): |
| target = seed.randrange(0, source) |
| for succ in G.successors(target): |
| G.add_edge(source, succ) |
| G.add_edge(source, target) |
| return G |
|
|
|
|
| @py_random_state(6) |
| @nx._dispatchable(graphs=None, returns_graph=True) |
| def scale_free_graph( |
| n, |
| alpha=0.41, |
| beta=0.54, |
| gamma=0.05, |
| delta_in=0.2, |
| delta_out=0, |
| seed=None, |
| initial_graph=None, |
| ): |
| """Returns a scale-free directed graph. |
| |
| Parameters |
| ---------- |
| n : integer |
| Number of nodes in graph |
| alpha : float |
| Probability for adding a new node connected to an existing node |
| chosen randomly according to the in-degree distribution. |
| beta : float |
| Probability for adding an edge between two existing nodes. |
| One existing node is chosen randomly according the in-degree |
| distribution and the other chosen randomly according to the out-degree |
| distribution. |
| gamma : float |
| Probability for adding a new node connected to an existing node |
| chosen randomly according to the out-degree distribution. |
| delta_in : float |
| Bias for choosing nodes from in-degree distribution. |
| delta_out : float |
| Bias for choosing nodes from out-degree distribution. |
| seed : integer, random_state, or None (default) |
| Indicator of random number generation state. |
| See :ref:`Randomness<randomness>`. |
| initial_graph : MultiDiGraph instance, optional |
| Build the scale-free graph starting from this initial MultiDiGraph, |
| if provided. |
| |
| Returns |
| ------- |
| MultiDiGraph |
| |
| Examples |
| -------- |
| Create a scale-free graph on one hundred nodes:: |
| |
| >>> G = nx.scale_free_graph(100) |
| |
| Notes |
| ----- |
| The sum of `alpha`, `beta`, and `gamma` must be 1. |
| |
| References |
| ---------- |
| .. [1] B. Bollobás, C. Borgs, J. Chayes, and O. Riordan, |
| Directed scale-free graphs, |
| Proceedings of the fourteenth annual ACM-SIAM Symposium on |
| Discrete Algorithms, 132--139, 2003. |
| """ |
|
|
| def _choose_node(candidates, node_list, delta): |
| if delta > 0: |
| bias_sum = len(node_list) * delta |
| p_delta = bias_sum / (bias_sum + len(candidates)) |
| if seed.random() < p_delta: |
| return seed.choice(node_list) |
| return seed.choice(candidates) |
|
|
| if initial_graph is not None and hasattr(initial_graph, "_adj"): |
| if not isinstance(initial_graph, nx.MultiDiGraph): |
| raise nx.NetworkXError("initial_graph must be a MultiDiGraph.") |
| G = initial_graph |
| else: |
| |
| G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 0)]) |
|
|
| if alpha <= 0: |
| raise ValueError("alpha must be > 0.") |
| if beta <= 0: |
| raise ValueError("beta must be > 0.") |
| if gamma <= 0: |
| raise ValueError("gamma must be > 0.") |
|
|
| if abs(alpha + beta + gamma - 1.0) >= 1e-9: |
| raise ValueError("alpha+beta+gamma must equal 1.") |
|
|
| if delta_in < 0: |
| raise ValueError("delta_in must be >= 0.") |
|
|
| if delta_out < 0: |
| raise ValueError("delta_out must be >= 0.") |
|
|
| |
| vs = sum((count * [idx] for idx, count in G.out_degree()), []) |
| ws = sum((count * [idx] for idx, count in G.in_degree()), []) |
|
|
| |
| node_list = list(G.nodes()) |
|
|
| |
| numeric_nodes = [n for n in node_list if isinstance(n, numbers.Number)] |
| if len(numeric_nodes) > 0: |
| |
| cursor = max(int(n.real) for n in numeric_nodes) + 1 |
| else: |
| |
| cursor = 0 |
|
|
| while len(G) < n: |
| r = seed.random() |
|
|
| |
| if r < alpha: |
| |
| |
| v = cursor |
| cursor += 1 |
| |
| node_list.append(v) |
| |
| w = _choose_node(ws, node_list, delta_in) |
|
|
| elif r < alpha + beta: |
| |
| |
| v = _choose_node(vs, node_list, delta_out) |
| |
| w = _choose_node(ws, node_list, delta_in) |
|
|
| else: |
| |
| |
| v = _choose_node(vs, node_list, delta_out) |
| |
| w = cursor |
| cursor += 1 |
| |
| node_list.append(w) |
|
|
| |
| G.add_edge(v, w) |
|
|
| |
| vs.append(v) |
| ws.append(w) |
|
|
| return G |
|
|
|
|
| @py_random_state(4) |
| @nx._dispatchable(graphs=None, returns_graph=True) |
| def random_uniform_k_out_graph(n, k, self_loops=True, with_replacement=True, seed=None): |
| """Returns a random `k`-out graph with uniform attachment. |
| |
| A random `k`-out graph with uniform attachment is a multidigraph |
| generated by the following algorithm. For each node *u*, choose |
| `k` nodes *v* uniformly at random (with replacement). Add a |
| directed edge joining *u* to *v*. |
| |
| Parameters |
| ---------- |
| n : int |
| The number of nodes in the returned graph. |
| |
| k : int |
| The out-degree of each node in the returned graph. |
| |
| self_loops : bool |
| If True, self-loops are allowed when generating the graph. |
| |
| with_replacement : bool |
| If True, neighbors are chosen with replacement and the |
| returned graph will be a directed multigraph. Otherwise, |
| neighbors are chosen without replacement and the returned graph |
| will be a directed graph. |
| |
| seed : integer, random_state, or None (default) |
| Indicator of random number generation state. |
| See :ref:`Randomness<randomness>`. |
| |
| Returns |
| ------- |
| NetworkX graph |
| A `k`-out-regular directed graph generated according to the |
| above algorithm. It will be a multigraph if and only if |
| `with_replacement` is True. |
| |
| Raises |
| ------ |
| ValueError |
| If `with_replacement` is False and `k` is greater than |
| `n`. |
| |
| See also |
| -------- |
| random_k_out_graph |
| |
| Notes |
| ----- |
| The return digraph or multidigraph may not be strongly connected, or |
| even weakly connected. |
| |
| If `with_replacement` is True, this function is similar to |
| :func:`random_k_out_graph`, if that function had parameter `alpha` |
| set to positive infinity. |
| |
| """ |
| if with_replacement: |
| create_using = nx.MultiDiGraph() |
|
|
| def sample(v, nodes): |
| if not self_loops: |
| nodes = nodes - {v} |
| return (seed.choice(list(nodes)) for i in range(k)) |
|
|
| else: |
| create_using = nx.DiGraph() |
|
|
| def sample(v, nodes): |
| if not self_loops: |
| nodes = nodes - {v} |
| return seed.sample(list(nodes), k) |
|
|
| G = nx.empty_graph(n, create_using) |
| nodes = set(G) |
| for u in G: |
| G.add_edges_from((u, v) for v in sample(u, nodes)) |
| return G |
|
|
|
|
| @nx._dispatchable(graphs=None, returns_graph=True) |
| def random_k_out_graph(n, k, alpha, self_loops=True, seed=None): |
| """Returns a random `k`-out graph with preferential attachment. |
| |
| .. versionchanged:: 3.5 |
| Different implementations will be used based on whether NumPy is |
| available. See Notes for details. |
| |
| A random `k`-out graph with preferential attachment is a |
| multidigraph generated by the following algorithm. |
| |
| 1. Begin with an empty digraph, and initially set each node to have |
| weight `alpha`. |
| 2. Choose a node `u` with out-degree less than `k` uniformly at |
| random. |
| 3. Choose a node `v` from with probability proportional to its |
| weight. |
| 4. Add a directed edge from `u` to `v`, and increase the weight |
| of `v` by one. |
| 5. If each node has out-degree `k`, halt, otherwise repeat from |
| step 2. |
| |
| For more information on this model of random graph, see [1]_. |
| |
| Parameters |
| ---------- |
| n : int |
| The number of nodes in the returned graph. |
| |
| k : int |
| The out-degree of each node in the returned graph. |
| |
| alpha : float |
| A positive :class:`float` representing the initial weight of |
| each vertex. A higher number means that in step 3 above, nodes |
| will be chosen more like a true uniformly random sample, and a |
| lower number means that nodes are more likely to be chosen as |
| their in-degree increases. If this parameter is not positive, a |
| :exc:`ValueError` is raised. |
| |
| self_loops : bool |
| If True, self-loops are allowed when generating the graph. |
| |
| seed : integer, random_state, or None (default) |
| Indicator of random number generation state. |
| See :ref:`Randomness<randomness>`. |
| |
| Returns |
| ------- |
| :class:`~networkx.classes.MultiDiGraph` |
| A `k`-out-regular multidigraph generated according to the above |
| algorithm. |
| |
| Raises |
| ------ |
| ValueError |
| If `alpha` is not positive. |
| |
| Notes |
| ----- |
| The returned multidigraph may not be strongly connected, or even |
| weakly connected. |
| |
| `random_k_out_graph` has two implementations: an array-based formulation that |
| uses `numpy` (``_random_k_out_graph_numpy``), and a pure-Python |
| implementation (``_random_k_out_graph_python``). |
| The NumPy implementation is more performant, especially for large `n`, and is |
| therefore used by default. If NumPy is not installed in the environment, |
| then the pure Python implementation is executed. |
| However, you can explicitly control which implementation is executed by directly |
| calling the corresponding function:: |
| |
| # Use numpy if available, else Python |
| nx.random_k_out_graph(1000, 5, alpha=1) |
| |
| # Use the numpy-based implementation (raises ImportError if numpy not installed) |
| nx.generators.directed._random_k_out_graph_numpy(1000, 5, alpha=1) |
| |
| # Use the Python-based implementation |
| nx.generators.directed._random_k_out_graph_python(1000, 5, alpha=1) |
| |
| References |
| ---------- |
| .. [1] Peterson, Nicholas R., and Boris Pittel. |
| "Distance between two random `k`-out digraphs, with and without preferential attachment." |
| arXiv preprint arXiv:1311.5961 (2013) <https://arxiv.org/abs/1311.5961>. |
| |
| """ |
| if alpha < 0: |
| raise ValueError("alpha must be positive") |
| try: |
| return _random_k_out_graph_numpy(n, k, alpha, self_loops, seed) |
| except ImportError: |
| return _random_k_out_graph_python(n, k, alpha, self_loops, seed) |
|
|
|
|
| @np_random_state(4) |
| def _random_k_out_graph_numpy(n, k, alpha, self_loops=True, seed=None): |
| import numpy as np |
|
|
| G = nx.empty_graph(n, create_using=nx.MultiDiGraph) |
| nodes = np.arange(n) |
| remaining_mask = np.full(n, True) |
| weights = np.full(n, alpha) |
| total_weight = n * alpha |
| out_strengths = np.zeros(n) |
|
|
| for i in range(k * n): |
| u = seed.choice(nodes[remaining_mask]) |
|
|
| if self_loops: |
| v = seed.choice(nodes, p=weights / total_weight) |
| else: |
| u_weight = weights[u] |
| weights[u] = 0 |
| v = seed.choice(nodes, p=weights / (total_weight - u_weight)) |
| weights[u] = u_weight |
|
|
| G.add_edge(u.item(), v.item()) |
| weights[v] += 1 |
| total_weight += 1 |
| out_strengths[u] += 1 |
| if out_strengths[u] == k: |
| remaining_mask[u] = False |
| return G |
|
|
|
|
| @py_random_state(4) |
| def _random_k_out_graph_python(n, k, alpha, self_loops=True, seed=None): |
| G = nx.empty_graph(n, create_using=nx.MultiDiGraph) |
| weights = Counter({v: alpha for v in G}) |
| out_strengths = Counter({v: 0 for v in G}) |
|
|
| for i in range(k * n): |
| u = seed.choice(list(out_strengths.keys())) |
| |
| |
| if not self_loops: |
| uweight = weights.pop(u) |
|
|
| v = weighted_choice(weights, seed=seed) |
|
|
| if not self_loops: |
| weights[u] = uweight |
|
|
| G.add_edge(u, v) |
| weights[v] += 1 |
| out_strengths[u] += 1 |
| if out_strengths[u] == k: |
| out_strengths.pop(u) |
| return G |
|
|