directed.py 16 KB

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