atlas.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. """
  2. Generators for the small graph atlas.
  3. """
  4. import gzip
  5. import importlib.resources
  6. from itertools import islice
  7. import networkx as nx
  8. __all__ = ["graph_atlas", "graph_atlas_g"]
  9. #: The total number of graphs in the atlas.
  10. #:
  11. #: The graphs are labeled starting from 0 and extending to (but not
  12. #: including) this number.
  13. NUM_GRAPHS = 1253
  14. #: The path to the data file containing the graph edge lists.
  15. #:
  16. #: This is the absolute path of the gzipped text file containing the
  17. #: edge list for each graph in the atlas. The file contains one entry
  18. #: per graph in the atlas, in sequential order, starting from graph
  19. #: number 0 and extending through graph number 1252 (see
  20. #: :data:`NUM_GRAPHS`). Each entry looks like
  21. #:
  22. #: .. sourcecode:: text
  23. #:
  24. #: GRAPH 6
  25. #: NODES 3
  26. #: 0 1
  27. #: 0 2
  28. #:
  29. #: where the first two lines are the graph's index in the atlas and the
  30. #: number of nodes in the graph, and the remaining lines are the edge
  31. #: list.
  32. #:
  33. #: This file was generated from a Python list of graphs via code like
  34. #: the following::
  35. #:
  36. #: import gzip
  37. #: from networkx.generators.atlas import graph_atlas_g
  38. #: from networkx.readwrite.edgelist import write_edgelist
  39. #:
  40. #: with gzip.open('atlas.dat.gz', 'wb') as f:
  41. #: for i, G in enumerate(graph_atlas_g()):
  42. #: f.write(bytes(f'GRAPH {i}\n', encoding='utf-8'))
  43. #: f.write(bytes(f'NODES {len(G)}\n', encoding='utf-8'))
  44. #: write_edgelist(G, f, data=False)
  45. #:
  46. # Path to the atlas file
  47. ATLAS_FILE = importlib.resources.files("networkx.generators") / "atlas.dat.gz"
  48. def _generate_graphs():
  49. """Sequentially read the file containing the edge list data for the
  50. graphs in the atlas and generate the graphs one at a time.
  51. This function reads the file given in :data:`.ATLAS_FILE`.
  52. """
  53. with gzip.open(ATLAS_FILE, "rb") as f:
  54. line = f.readline()
  55. while line and line.startswith(b"GRAPH"):
  56. # The first two lines of each entry tell us the index of the
  57. # graph in the list and the number of nodes in the graph.
  58. # They look like this:
  59. #
  60. # GRAPH 3
  61. # NODES 2
  62. #
  63. graph_index = int(line[6:].rstrip())
  64. line = f.readline()
  65. num_nodes = int(line[6:].rstrip())
  66. # The remaining lines contain the edge list, until the next
  67. # GRAPH line (or until the end of the file).
  68. edgelist = []
  69. line = f.readline()
  70. while line and not line.startswith(b"GRAPH"):
  71. edgelist.append(line.rstrip())
  72. line = f.readline()
  73. G = nx.Graph()
  74. G.name = f"G{graph_index}"
  75. G.add_nodes_from(range(num_nodes))
  76. G.add_edges_from(tuple(map(int, e.split())) for e in edgelist)
  77. yield G
  78. @nx._dispatchable(graphs=None, returns_graph=True)
  79. def graph_atlas(i):
  80. """Returns graph number `i` from the Graph Atlas.
  81. For more information, see :func:`.graph_atlas_g`.
  82. Parameters
  83. ----------
  84. i : int
  85. The index of the graph from the atlas to get. The graph at index
  86. 0 is assumed to be the null graph.
  87. Returns
  88. -------
  89. list
  90. A list of :class:`~networkx.Graph` objects, the one at index *i*
  91. corresponding to the graph *i* in the Graph Atlas.
  92. See also
  93. --------
  94. graph_atlas_g
  95. Notes
  96. -----
  97. The time required by this function increases linearly with the
  98. argument `i`, since it reads a large file sequentially in order to
  99. generate the graph [1]_.
  100. References
  101. ----------
  102. .. [1] Ronald C. Read and Robin J. Wilson, *An Atlas of Graphs*.
  103. Oxford University Press, 1998.
  104. """
  105. if not (0 <= i < NUM_GRAPHS):
  106. raise ValueError(f"index must be between 0 and {NUM_GRAPHS}")
  107. return next(islice(_generate_graphs(), i, None))
  108. @nx._dispatchable(graphs=None, returns_graph=True)
  109. def graph_atlas_g():
  110. """Returns the list of all graphs with up to seven nodes named in the
  111. Graph Atlas.
  112. The graphs are listed in increasing order by
  113. 1. number of nodes,
  114. 2. number of edges,
  115. 3. degree sequence (for example 111223 < 112222),
  116. 4. number of automorphisms,
  117. in that order, with three exceptions as described in the *Notes*
  118. section below. This causes the list to correspond with the index of
  119. the graphs in the Graph Atlas [atlas]_, with the first graph,
  120. ``G[0]``, being the null graph.
  121. Returns
  122. -------
  123. list
  124. A list of :class:`~networkx.Graph` objects, the one at index *i*
  125. corresponding to the graph *i* in the Graph Atlas.
  126. Examples
  127. --------
  128. >>> from pprint import pprint
  129. >>> atlas = nx.graph_atlas_g()
  130. There are 1253 graphs in the atlas
  131. >>> len(atlas)
  132. 1253
  133. The number of graphs with *n* nodes, where *n* ranges from 0 to 7:
  134. >>> from collections import Counter
  135. >>> num_nodes_per_graph = [len(G) for G in atlas]
  136. >>> Counter(num_nodes_per_graph)
  137. Counter({7: 1044, 6: 156, 5: 34, 4: 11, 3: 4, 2: 2, 0: 1, 1: 1})
  138. Since the atlas is ordered by the number of nodes in the graph, all graphs
  139. with *n* nodes can be obtained by slicing the atlas. For example, all
  140. graphs with 5 nodes:
  141. >>> G5_list = atlas[19:53]
  142. >>> all(len(G) == 5 for G in G5_list)
  143. True
  144. Or all graphs with at least 3 nodes but fewer than 7 nodes:
  145. >>> G3_6_list = atlas[4:209]
  146. More generally, the indices that partition the atlas by the number of nodes
  147. per graph:
  148. >>> import itertools
  149. >>> partition_indices = [0] + list(
  150. ... itertools.accumulate(Counter(num_nodes_per_graph).values()) # cumsum
  151. ... )
  152. >>> partition_indices
  153. [0, 1, 2, 4, 8, 19, 53, 209, 1253]
  154. >>> partition_mapping = dict(enumerate(itertools.pairwise(partition_indices)))
  155. >>> pprint(partition_mapping)
  156. {0: (0, 1),
  157. 1: (1, 2),
  158. 2: (2, 4),
  159. 3: (4, 8),
  160. 4: (8, 19),
  161. 5: (19, 53),
  162. 6: (53, 209),
  163. 7: (209, 1253)}
  164. See also
  165. --------
  166. graph_atlas
  167. Notes
  168. -----
  169. This function may be expensive in both time and space, since it
  170. reads a large file sequentially in order to populate the list.
  171. Although the NetworkX atlas functions match the order of graphs
  172. given in the "Atlas of Graphs" book, there are (at least) three
  173. errors in the ordering described in the book. The following three
  174. pairs of nodes violate the lexicographically nondecreasing sorted
  175. degree sequence rule:
  176. - graphs 55 and 56 with degree sequences 001111 and 000112,
  177. - graphs 1007 and 1008 with degree sequences 3333444 and 3333336,
  178. - graphs 1012 and 1213 with degree sequences 1244555 and 1244456.
  179. References
  180. ----------
  181. .. [atlas] Ronald C. Read and Robin J. Wilson,
  182. *An Atlas of Graphs*.
  183. Oxford University Press, 1998.
  184. """
  185. return list(_generate_graphs())