directed.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. """
  2. Generators for some directed graphs, including growing network (GN) graphs and
  3. scale-free graphs.
  4. """
  5. import numbers
  6. from collections import Counter
  7. import networkx as nx
  8. from networkx.generators.classic import empty_graph
  9. from networkx.utils import (
  10. discrete_sequence,
  11. np_random_state,
  12. py_random_state,
  13. weighted_choice,
  14. )
  15. __all__ = [
  16. "gn_graph",
  17. "gnc_graph",
  18. "gnr_graph",
  19. "random_k_out_graph",
  20. "scale_free_graph",
  21. ]
  22. @py_random_state(3)
  23. @nx._dispatchable(graphs=None, returns_graph=True)
  24. def gn_graph(n, kernel=None, create_using=None, seed=None):
  25. """Returns the growing network (GN) digraph with `n` nodes.
  26. The GN graph is built by adding nodes one at a time with a link to one
  27. previously added node. The target node for the link is chosen with
  28. probability based on degree. The default attachment kernel is a linear
  29. function of the degree of a node.
  30. The graph is always a (directed) tree.
  31. Parameters
  32. ----------
  33. n : int
  34. The number of nodes for the generated graph.
  35. kernel : function
  36. The attachment kernel.
  37. create_using : NetworkX graph constructor, optional (default DiGraph)
  38. Graph type to create. If graph instance, then cleared before populated.
  39. seed : integer, random_state, or None (default)
  40. Indicator of random number generation state.
  41. See :ref:`Randomness<randomness>`.
  42. Examples
  43. --------
  44. To create the undirected GN graph, use the :meth:`~DiGraph.to_directed`
  45. method::
  46. >>> D = nx.gn_graph(10) # the GN graph
  47. >>> G = D.to_undirected() # the undirected version
  48. To specify an attachment kernel, use the `kernel` keyword argument::
  49. >>> D = nx.gn_graph(10, kernel=lambda x: x**1.5) # A_k = k^1.5
  50. References
  51. ----------
  52. .. [1] P. L. Krapivsky and S. Redner,
  53. Organization of Growing Random Networks,
  54. Phys. Rev. E, 63, 066123, 2001.
  55. """
  56. G = empty_graph(1, create_using, default=nx.DiGraph)
  57. if not G.is_directed():
  58. raise nx.NetworkXError("create_using must indicate a Directed Graph")
  59. if kernel is None:
  60. def kernel(x):
  61. return x
  62. if n == 1:
  63. return G
  64. G.add_edge(1, 0) # get started
  65. ds = [1, 1] # degree sequence
  66. for source in range(2, n):
  67. # compute distribution from kernel and degree
  68. dist = [kernel(d) for d in ds]
  69. # choose target from discrete distribution
  70. target = discrete_sequence(1, distribution=dist, seed=seed)[0]
  71. G.add_edge(source, target)
  72. ds.append(1) # the source has only one link (degree one)
  73. ds[target] += 1 # add one to the target link degree
  74. return G
  75. @py_random_state(3)
  76. @nx._dispatchable(graphs=None, returns_graph=True)
  77. def gnr_graph(n, p, create_using=None, seed=None):
  78. """Returns the growing network with redirection (GNR) digraph with `n`
  79. nodes and redirection probability `p`.
  80. The GNR graph is built by adding nodes one at a time with a link to one
  81. previously added node. The previous target node is chosen uniformly at
  82. random. With probability `p` the link is instead "redirected" to the
  83. successor node of the target.
  84. The graph is always a (directed) tree.
  85. Parameters
  86. ----------
  87. n : int
  88. The number of nodes for the generated graph.
  89. p : float
  90. The redirection probability.
  91. create_using : NetworkX graph constructor, optional (default DiGraph)
  92. Graph type to create. If graph instance, then cleared before populated.
  93. seed : integer, random_state, or None (default)
  94. Indicator of random number generation state.
  95. See :ref:`Randomness<randomness>`.
  96. Examples
  97. --------
  98. To create the undirected GNR graph, use the :meth:`~DiGraph.to_directed`
  99. method::
  100. >>> D = nx.gnr_graph(10, 0.5) # the GNR graph
  101. >>> G = D.to_undirected() # the undirected version
  102. References
  103. ----------
  104. .. [1] P. L. Krapivsky and S. Redner,
  105. Organization of Growing Random Networks,
  106. Phys. Rev. E, 63, 066123, 2001.
  107. """
  108. G = empty_graph(1, create_using, default=nx.DiGraph)
  109. if not G.is_directed():
  110. raise nx.NetworkXError("create_using must indicate a Directed Graph")
  111. if n == 1:
  112. return G
  113. for source in range(1, n):
  114. target = seed.randrange(0, source)
  115. if seed.random() < p and target != 0:
  116. target = next(G.successors(target))
  117. G.add_edge(source, target)
  118. return G
  119. @py_random_state(2)
  120. @nx._dispatchable(graphs=None, returns_graph=True)
  121. def gnc_graph(n, create_using=None, seed=None):
  122. """Returns the growing network with copying (GNC) digraph with `n` nodes.
  123. The GNC graph is built by adding nodes one at a time with a link to one
  124. previously added node (chosen uniformly at random) and to all of that
  125. node's successors.
  126. Parameters
  127. ----------
  128. n : int
  129. The number of nodes for the generated graph.
  130. create_using : NetworkX graph constructor, optional (default DiGraph)
  131. Graph type to create. If graph instance, then cleared before populated.
  132. seed : integer, random_state, or None (default)
  133. Indicator of random number generation state.
  134. See :ref:`Randomness<randomness>`.
  135. References
  136. ----------
  137. .. [1] P. L. Krapivsky and S. Redner,
  138. Network Growth by Copying,
  139. Phys. Rev. E, 71, 036118, 2005k.},
  140. """
  141. G = empty_graph(1, create_using, default=nx.DiGraph)
  142. if not G.is_directed():
  143. raise nx.NetworkXError("create_using must indicate a Directed Graph")
  144. if n == 1:
  145. return G
  146. for source in range(1, n):
  147. target = seed.randrange(0, source)
  148. for succ in G.successors(target):
  149. G.add_edge(source, succ)
  150. G.add_edge(source, target)
  151. return G
  152. @py_random_state(6)
  153. @nx._dispatchable(graphs=None, returns_graph=True)
  154. def scale_free_graph(
  155. n,
  156. alpha=0.41,
  157. beta=0.54,
  158. gamma=0.05,
  159. delta_in=0.2,
  160. delta_out=0,
  161. seed=None,
  162. initial_graph=None,
  163. ):
  164. """Returns a scale-free directed graph.
  165. Parameters
  166. ----------
  167. n : integer
  168. Number of nodes in graph
  169. alpha : float
  170. Probability for adding a new node connected to an existing node
  171. chosen randomly according to the in-degree distribution.
  172. beta : float
  173. Probability for adding an edge between two existing nodes.
  174. One existing node is chosen randomly according the in-degree
  175. distribution and the other chosen randomly according to the out-degree
  176. distribution.
  177. gamma : float
  178. Probability for adding a new node connected to an existing node
  179. chosen randomly according to the out-degree distribution.
  180. delta_in : float
  181. Bias for choosing nodes from in-degree distribution.
  182. delta_out : float
  183. Bias for choosing nodes from out-degree distribution.
  184. seed : integer, random_state, or None (default)
  185. Indicator of random number generation state.
  186. See :ref:`Randomness<randomness>`.
  187. initial_graph : MultiDiGraph instance, optional
  188. Build the scale-free graph starting from this initial MultiDiGraph,
  189. if provided.
  190. Returns
  191. -------
  192. MultiDiGraph
  193. Examples
  194. --------
  195. Create a scale-free graph on one hundred nodes::
  196. >>> G = nx.scale_free_graph(100)
  197. Notes
  198. -----
  199. The sum of `alpha`, `beta`, and `gamma` must be 1.
  200. References
  201. ----------
  202. .. [1] B. Bollobás, C. Borgs, J. Chayes, and O. Riordan,
  203. Directed scale-free graphs,
  204. Proceedings of the fourteenth annual ACM-SIAM Symposium on
  205. Discrete Algorithms, 132--139, 2003.
  206. """
  207. def _choose_node(candidates, node_list, delta):
  208. if delta > 0:
  209. bias_sum = len(node_list) * delta
  210. p_delta = bias_sum / (bias_sum + len(candidates))
  211. if seed.random() < p_delta:
  212. return seed.choice(node_list)
  213. return seed.choice(candidates)
  214. if initial_graph is not None and hasattr(initial_graph, "_adj"):
  215. if not isinstance(initial_graph, nx.MultiDiGraph):
  216. raise nx.NetworkXError("initial_graph must be a MultiDiGraph.")
  217. G = initial_graph
  218. else:
  219. # Start with 3-cycle
  220. G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 0)])
  221. if alpha <= 0:
  222. raise ValueError("alpha must be > 0.")
  223. if beta <= 0:
  224. raise ValueError("beta must be > 0.")
  225. if gamma <= 0:
  226. raise ValueError("gamma must be > 0.")
  227. if abs(alpha + beta + gamma - 1.0) >= 1e-9:
  228. raise ValueError("alpha+beta+gamma must equal 1.")
  229. if delta_in < 0:
  230. raise ValueError("delta_in must be >= 0.")
  231. if delta_out < 0:
  232. raise ValueError("delta_out must be >= 0.")
  233. # pre-populate degree states
  234. vs = sum((count * [idx] for idx, count in G.out_degree()), [])
  235. ws = sum((count * [idx] for idx, count in G.in_degree()), [])
  236. # pre-populate node state
  237. node_list = list(G.nodes())
  238. # see if there already are number-based nodes
  239. numeric_nodes = [n for n in node_list if isinstance(n, numbers.Number)]
  240. if len(numeric_nodes) > 0:
  241. # set cursor for new nodes appropriately
  242. cursor = max(int(n.real) for n in numeric_nodes) + 1
  243. else:
  244. # or start at zero
  245. cursor = 0
  246. while len(G) < n:
  247. r = seed.random()
  248. # random choice in alpha,beta,gamma ranges
  249. if r < alpha:
  250. # alpha
  251. # add new node v
  252. v = cursor
  253. cursor += 1
  254. # also add to node state
  255. node_list.append(v)
  256. # choose w according to in-degree and delta_in
  257. w = _choose_node(ws, node_list, delta_in)
  258. elif r < alpha + beta:
  259. # beta
  260. # choose v according to out-degree and delta_out
  261. v = _choose_node(vs, node_list, delta_out)
  262. # choose w according to in-degree and delta_in
  263. w = _choose_node(ws, node_list, delta_in)
  264. else:
  265. # gamma
  266. # choose v according to out-degree and delta_out
  267. v = _choose_node(vs, node_list, delta_out)
  268. # add new node w
  269. w = cursor
  270. cursor += 1
  271. # also add to node state
  272. node_list.append(w)
  273. # add edge to graph
  274. G.add_edge(v, w)
  275. # update degree states
  276. vs.append(v)
  277. ws.append(w)
  278. return G
  279. @py_random_state(4)
  280. @nx._dispatchable(graphs=None, returns_graph=True)
  281. def random_uniform_k_out_graph(n, k, self_loops=True, with_replacement=True, seed=None):
  282. """Returns a random `k`-out graph with uniform attachment.
  283. A random `k`-out graph with uniform attachment is a multidigraph
  284. generated by the following algorithm. For each node *u*, choose
  285. `k` nodes *v* uniformly at random (with replacement). Add a
  286. directed edge joining *u* to *v*.
  287. Parameters
  288. ----------
  289. n : int
  290. The number of nodes in the returned graph.
  291. k : int
  292. The out-degree of each node in the returned graph.
  293. self_loops : bool
  294. If True, self-loops are allowed when generating the graph.
  295. with_replacement : bool
  296. If True, neighbors are chosen with replacement and the
  297. returned graph will be a directed multigraph. Otherwise,
  298. neighbors are chosen without replacement and the returned graph
  299. will be a directed graph.
  300. seed : integer, random_state, or None (default)
  301. Indicator of random number generation state.
  302. See :ref:`Randomness<randomness>`.
  303. Returns
  304. -------
  305. NetworkX graph
  306. A `k`-out-regular directed graph generated according to the
  307. above algorithm. It will be a multigraph if and only if
  308. `with_replacement` is True.
  309. Raises
  310. ------
  311. ValueError
  312. If `with_replacement` is False and `k` is greater than
  313. `n`.
  314. See also
  315. --------
  316. random_k_out_graph
  317. Notes
  318. -----
  319. The return digraph or multidigraph may not be strongly connected, or
  320. even weakly connected.
  321. If `with_replacement` is True, this function is similar to
  322. :func:`random_k_out_graph`, if that function had parameter `alpha`
  323. set to positive infinity.
  324. """
  325. if with_replacement:
  326. create_using = nx.MultiDiGraph()
  327. def sample(v, nodes):
  328. if not self_loops:
  329. nodes = nodes - {v}
  330. return (seed.choice(list(nodes)) for i in range(k))
  331. else:
  332. create_using = nx.DiGraph()
  333. def sample(v, nodes):
  334. if not self_loops:
  335. nodes = nodes - {v}
  336. return seed.sample(list(nodes), k)
  337. G = nx.empty_graph(n, create_using)
  338. nodes = set(G)
  339. for u in G:
  340. G.add_edges_from((u, v) for v in sample(u, nodes))
  341. return G
  342. @nx._dispatchable(graphs=None, returns_graph=True)
  343. def random_k_out_graph(n, k, alpha, self_loops=True, seed=None):
  344. """Returns a random `k`-out graph with preferential attachment.
  345. .. versionchanged:: 3.5
  346. Different implementations will be used based on whether NumPy is
  347. available. See Notes for details.
  348. A random `k`-out graph with preferential attachment is a
  349. multidigraph generated by the following algorithm.
  350. 1. Begin with an empty digraph, and initially set each node to have
  351. weight `alpha`.
  352. 2. Choose a node `u` with out-degree less than `k` uniformly at
  353. random.
  354. 3. Choose a node `v` from with probability proportional to its
  355. weight.
  356. 4. Add a directed edge from `u` to `v`, and increase the weight
  357. of `v` by one.
  358. 5. If each node has out-degree `k`, halt, otherwise repeat from
  359. step 2.
  360. For more information on this model of random graph, see [1]_.
  361. Parameters
  362. ----------
  363. n : int
  364. The number of nodes in the returned graph.
  365. k : int
  366. The out-degree of each node in the returned graph.
  367. alpha : float
  368. A positive :class:`float` representing the initial weight of
  369. each vertex. A higher number means that in step 3 above, nodes
  370. will be chosen more like a true uniformly random sample, and a
  371. lower number means that nodes are more likely to be chosen as
  372. their in-degree increases. If this parameter is not positive, a
  373. :exc:`ValueError` is raised.
  374. self_loops : bool
  375. If True, self-loops are allowed when generating the graph.
  376. seed : integer, random_state, or None (default)
  377. Indicator of random number generation state.
  378. See :ref:`Randomness<randomness>`.
  379. Returns
  380. -------
  381. :class:`~networkx.classes.MultiDiGraph`
  382. A `k`-out-regular multidigraph generated according to the above
  383. algorithm.
  384. Raises
  385. ------
  386. ValueError
  387. If `alpha` is not positive.
  388. Notes
  389. -----
  390. The returned multidigraph may not be strongly connected, or even
  391. weakly connected.
  392. `random_k_out_graph` has two implementations: an array-based formulation that
  393. uses `numpy` (``_random_k_out_graph_numpy``), and a pure-Python
  394. implementation (``_random_k_out_graph_python``).
  395. The NumPy implementation is more performant, especially for large `n`, and is
  396. therefore used by default. If NumPy is not installed in the environment,
  397. then the pure Python implementation is executed.
  398. However, you can explicitly control which implementation is executed by directly
  399. calling the corresponding function::
  400. # Use numpy if available, else Python
  401. nx.random_k_out_graph(1000, 5, alpha=1)
  402. # Use the numpy-based implementation (raises ImportError if numpy not installed)
  403. nx.generators.directed._random_k_out_graph_numpy(1000, 5, alpha=1)
  404. # Use the Python-based implementation
  405. nx.generators.directed._random_k_out_graph_python(1000, 5, alpha=1)
  406. References
  407. ----------
  408. .. [1] Peterson, Nicholas R., and Boris Pittel.
  409. "Distance between two random `k`-out digraphs, with and without preferential attachment."
  410. arXiv preprint arXiv:1311.5961 (2013) <https://arxiv.org/abs/1311.5961>.
  411. """
  412. if alpha < 0:
  413. raise ValueError("alpha must be positive")
  414. try: # Use numpy if available, otherwise fall back to pure Python implementation
  415. return _random_k_out_graph_numpy(n, k, alpha, self_loops, seed)
  416. except ImportError:
  417. return _random_k_out_graph_python(n, k, alpha, self_loops, seed)
  418. @np_random_state(4)
  419. def _random_k_out_graph_numpy(n, k, alpha, self_loops=True, seed=None):
  420. import numpy as np
  421. G = nx.empty_graph(n, create_using=nx.MultiDiGraph)
  422. nodes = np.arange(n)
  423. remaining_mask = np.full(n, True)
  424. weights = np.full(n, alpha)
  425. total_weight = n * alpha
  426. out_strengths = np.zeros(n)
  427. for i in range(k * n):
  428. u = seed.choice(nodes[remaining_mask])
  429. if self_loops:
  430. v = seed.choice(nodes, p=weights / total_weight)
  431. else: # Ignore weight of u when selecting v
  432. u_weight = weights[u]
  433. weights[u] = 0
  434. v = seed.choice(nodes, p=weights / (total_weight - u_weight))
  435. weights[u] = u_weight
  436. G.add_edge(u.item(), v.item())
  437. weights[v] += 1
  438. total_weight += 1
  439. out_strengths[u] += 1
  440. if out_strengths[u] == k:
  441. remaining_mask[u] = False
  442. return G
  443. @py_random_state(4)
  444. def _random_k_out_graph_python(n, k, alpha, self_loops=True, seed=None):
  445. G = nx.empty_graph(n, create_using=nx.MultiDiGraph)
  446. weights = Counter({v: alpha for v in G})
  447. out_strengths = Counter({v: 0 for v in G})
  448. for i in range(k * n):
  449. u = seed.choice(list(out_strengths.keys()))
  450. # If self-loops are not allowed, make the source node `u` have
  451. # weight zero.
  452. if not self_loops:
  453. uweight = weights.pop(u)
  454. v = weighted_choice(weights, seed=seed)
  455. if not self_loops:
  456. weights[u] = uweight
  457. G.add_edge(u, v)
  458. weights[v] += 1
  459. out_strengths[u] += 1
  460. if out_strengths[u] == k:
  461. out_strengths.pop(u)
  462. return G