expanders.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. """Provides explicit constructions of expander graphs."""
  2. import itertools
  3. import networkx as nx
  4. __all__ = [
  5. "margulis_gabber_galil_graph",
  6. "chordal_cycle_graph",
  7. "paley_graph",
  8. "maybe_regular_expander",
  9. "maybe_regular_expander_graph",
  10. "is_regular_expander",
  11. "random_regular_expander_graph",
  12. ]
  13. # Other discrete torus expanders can be constructed by using the following edge
  14. # sets. For more information, see Chapter 4, "Expander Graphs", in
  15. # "Pseudorandomness", by Salil Vadhan.
  16. #
  17. # For a directed expander, add edges from (x, y) to:
  18. #
  19. # (x, y),
  20. # ((x + 1) % n, y),
  21. # (x, (y + 1) % n),
  22. # (x, (x + y) % n),
  23. # (-y % n, x)
  24. #
  25. # For an undirected expander, add the reverse edges.
  26. #
  27. # Also appearing in the paper of Gabber and Galil:
  28. #
  29. # (x, y),
  30. # (x, (x + y) % n),
  31. # (x, (x + y + 1) % n),
  32. # ((x + y) % n, y),
  33. # ((x + y + 1) % n, y)
  34. #
  35. # and:
  36. #
  37. # (x, y),
  38. # ((x + 2*y) % n, y),
  39. # ((x + (2*y + 1)) % n, y),
  40. # ((x + (2*y + 2)) % n, y),
  41. # (x, (y + 2*x) % n),
  42. # (x, (y + (2*x + 1)) % n),
  43. # (x, (y + (2*x + 2)) % n),
  44. #
  45. @nx._dispatchable(graphs=None, returns_graph=True)
  46. def margulis_gabber_galil_graph(n, create_using=None):
  47. r"""Returns the Margulis-Gabber-Galil undirected MultiGraph on `n^2` nodes.
  48. The undirected MultiGraph is regular with degree `8`. Nodes are integer
  49. pairs. The second-largest eigenvalue of the adjacency matrix of the graph
  50. is at most `5 \sqrt{2}`, regardless of `n`.
  51. Parameters
  52. ----------
  53. n : int
  54. Determines the number of nodes in the graph: `n^2`.
  55. create_using : NetworkX graph constructor, optional (default MultiGraph)
  56. Graph type to create. If graph instance, then cleared before populated.
  57. Returns
  58. -------
  59. G : graph
  60. The constructed undirected multigraph.
  61. Raises
  62. ------
  63. NetworkXError
  64. If the graph is directed or not a multigraph.
  65. """
  66. G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
  67. if G.is_directed() or not G.is_multigraph():
  68. msg = "`create_using` must be an undirected multigraph."
  69. raise nx.NetworkXError(msg)
  70. for x, y in itertools.product(range(n), repeat=2):
  71. for u, v in (
  72. ((x + 2 * y) % n, y),
  73. ((x + (2 * y + 1)) % n, y),
  74. (x, (y + 2 * x) % n),
  75. (x, (y + (2 * x + 1)) % n),
  76. ):
  77. G.add_edge((x, y), (u, v))
  78. G.graph["name"] = f"margulis_gabber_galil_graph({n})"
  79. return G
  80. @nx._dispatchable(graphs=None, returns_graph=True)
  81. def chordal_cycle_graph(p, create_using=None):
  82. """Returns the chordal cycle graph on `p` nodes.
  83. The returned graph is a cycle graph on `p` nodes with chords joining each
  84. vertex `x` to its inverse modulo `p`. This graph is a (mildly explicit)
  85. 3-regular expander [1]_.
  86. `p` *must* be a prime number.
  87. Parameters
  88. ----------
  89. p : a prime number
  90. The number of vertices in the graph. This also indicates where the
  91. chordal edges in the cycle will be created.
  92. create_using : NetworkX graph constructor, optional (default=nx.Graph)
  93. Graph type to create. If graph instance, then cleared before populated.
  94. Returns
  95. -------
  96. G : graph
  97. The constructed undirected multigraph.
  98. Raises
  99. ------
  100. NetworkXError
  101. If `create_using` indicates directed or not a multigraph.
  102. References
  103. ----------
  104. .. [1] Theorem 4.4.2 in A. Lubotzky. "Discrete groups, expanding graphs and
  105. invariant measures", volume 125 of Progress in Mathematics.
  106. Birkhäuser Verlag, Basel, 1994.
  107. """
  108. G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
  109. if G.is_directed() or not G.is_multigraph():
  110. msg = "`create_using` must be an undirected multigraph."
  111. raise nx.NetworkXError(msg)
  112. for x in range(p):
  113. left = (x - 1) % p
  114. right = (x + 1) % p
  115. # Here we apply Fermat's Little Theorem to compute the multiplicative
  116. # inverse of x in Z/pZ. By Fermat's Little Theorem,
  117. #
  118. # x^p = x (mod p)
  119. #
  120. # Therefore,
  121. #
  122. # x * x^(p - 2) = 1 (mod p)
  123. #
  124. # The number 0 is a special case: we just let its inverse be itself.
  125. chord = pow(x, p - 2, p) if x > 0 else 0
  126. for y in (left, right, chord):
  127. G.add_edge(x, y)
  128. G.graph["name"] = f"chordal_cycle_graph({p})"
  129. return G
  130. @nx._dispatchable(graphs=None, returns_graph=True)
  131. def paley_graph(p, create_using=None):
  132. r"""Returns the Paley $\frac{(p-1)}{2}$ -regular graph on $p$ nodes.
  133. The returned graph is a graph on $\mathbb{Z}/p\mathbb{Z}$ with edges between $x$ and $y$
  134. if and only if $x-y$ is a nonzero square in $\mathbb{Z}/p\mathbb{Z}$.
  135. If $p \equiv 1 \pmod 4$, $-1$ is a square in
  136. $\mathbb{Z}/p\mathbb{Z}$ and therefore $x-y$ is a square if and
  137. only if $y-x$ is also a square, i.e the edges in the Paley graph are symmetric.
  138. If $p \equiv 3 \pmod 4$, $-1$ is not a square in $\mathbb{Z}/p\mathbb{Z}$
  139. and therefore either $x-y$ or $y-x$ is a square in $\mathbb{Z}/p\mathbb{Z}$ but not both.
  140. Note that a more general definition of Paley graphs extends this construction
  141. to graphs over $q=p^n$ vertices, by using the finite field $F_q$ instead of
  142. $\mathbb{Z}/p\mathbb{Z}$.
  143. This construction requires to compute squares in general finite fields and is
  144. not what is implemented here (i.e `paley_graph(25)` does not return the true
  145. Paley graph associated with $5^2$).
  146. Parameters
  147. ----------
  148. p : int, an odd prime number.
  149. create_using : NetworkX graph constructor, optional (default=nx.Graph)
  150. Graph type to create. If graph instance, then cleared before populated.
  151. Returns
  152. -------
  153. G : graph
  154. The constructed directed graph.
  155. Raises
  156. ------
  157. NetworkXError
  158. If the graph is a multigraph.
  159. References
  160. ----------
  161. Chapter 13 in B. Bollobas, Random Graphs. Second edition.
  162. Cambridge Studies in Advanced Mathematics, 73.
  163. Cambridge University Press, Cambridge (2001).
  164. """
  165. G = nx.empty_graph(0, create_using, default=nx.DiGraph)
  166. if G.is_multigraph():
  167. msg = "`create_using` cannot be a multigraph."
  168. raise nx.NetworkXError(msg)
  169. # Compute the squares in Z/pZ.
  170. # Make it a set to uniquify (there are exactly (p-1)/2 squares in Z/pZ
  171. # when is prime).
  172. square_set = {(x**2) % p for x in range(1, p) if (x**2) % p != 0}
  173. for x in range(p):
  174. for x2 in square_set:
  175. G.add_edge(x, (x + x2) % p)
  176. G.graph["name"] = f"paley({p})"
  177. return G
  178. @nx.utils.decorators.np_random_state("seed")
  179. @nx._dispatchable(graphs=None, returns_graph=True)
  180. def maybe_regular_expander_graph(n, d, *, create_using=None, max_tries=100, seed=None):
  181. r"""Utility for creating a random regular expander.
  182. Returns a random $d$-regular graph on $n$ nodes which is an expander
  183. graph with very good probability.
  184. Parameters
  185. ----------
  186. n : int
  187. The number of nodes.
  188. d : int
  189. The degree of each node.
  190. create_using : Graph Instance or Constructor
  191. Indicator of type of graph to return.
  192. If a Graph-type instance, then clear and use it.
  193. If a constructor, call it to create an empty graph.
  194. Use the Graph constructor by default.
  195. max_tries : int. (default: 100)
  196. The number of allowed loops when generating each independent cycle
  197. seed : (default: None)
  198. Seed used to set random number generation state. See :ref`Randomness<randomness>`.
  199. Notes
  200. -----
  201. The nodes are numbered from $0$ to $n - 1$.
  202. The graph is generated by taking $d / 2$ random independent cycles.
  203. Joel Friedman proved that in this model the resulting
  204. graph is an expander with probability
  205. $1 - O(n^{-\tau})$ where $\tau = \lceil (\sqrt{d - 1}) / 2 \rceil - 1$. [1]_
  206. Examples
  207. --------
  208. >>> G = nx.maybe_regular_expander_graph(n=200, d=6, seed=8020)
  209. Returns
  210. -------
  211. G : graph
  212. The constructed undirected graph.
  213. Raises
  214. ------
  215. NetworkXError
  216. If $d % 2 != 0$ as the degree must be even.
  217. If $n - 1$ is less than $ 2d $ as the graph is complete at most.
  218. If max_tries is reached
  219. See Also
  220. --------
  221. is_regular_expander
  222. random_regular_expander_graph
  223. References
  224. ----------
  225. .. [1] Joel Friedman,
  226. A Proof of Alon's Second Eigenvalue Conjecture and Related Problems, 2004
  227. https://arxiv.org/abs/cs/0405020
  228. """
  229. import numpy as np
  230. if n < 1:
  231. raise nx.NetworkXError("n must be a positive integer")
  232. if not (d >= 2):
  233. raise nx.NetworkXError("d must be greater than or equal to 2")
  234. if not (d % 2 == 0):
  235. raise nx.NetworkXError("d must be even")
  236. if not (n - 1 >= d):
  237. raise nx.NetworkXError(
  238. f"Need n-1>= d to have room for {d // 2} independent cycles with {n} nodes"
  239. )
  240. G = nx.empty_graph(n, create_using)
  241. if n < 2:
  242. return G
  243. cycles = []
  244. edges = set()
  245. # Create d / 2 cycles
  246. for i in range(d // 2):
  247. iterations = max_tries
  248. # Make sure the cycles are independent to have a regular graph
  249. while len(edges) != (i + 1) * n:
  250. iterations -= 1
  251. # Faster than random.permutation(n) since there are only
  252. # (n-1)! distinct cycles against n! permutations of size n
  253. cycle = seed.permutation(n - 1).tolist()
  254. cycle.append(n - 1)
  255. new_edges = {
  256. (u, v)
  257. for u, v in nx.utils.pairwise(cycle, cyclic=True)
  258. if (u, v) not in edges and (v, u) not in edges
  259. }
  260. # If the new cycle has no edges in common with previous cycles
  261. # then add it to the list otherwise try again
  262. if len(new_edges) == n:
  263. cycles.append(cycle)
  264. edges.update(new_edges)
  265. if iterations == 0:
  266. msg = "Too many iterations in maybe_regular_expander_graph"
  267. raise nx.NetworkXError(msg)
  268. G.add_edges_from(edges)
  269. return G
  270. def maybe_regular_expander(n, d, *, create_using=None, max_tries=100, seed=None):
  271. """
  272. .. deprecated:: 3.6
  273. `maybe_regular_expander` is a deprecated alias
  274. for `maybe_regular_expander_graph`.
  275. Use `maybe_regular_expander_graph` instead.
  276. """
  277. import warnings
  278. warnings.warn(
  279. "maybe_regular_expander is deprecated, "
  280. "use `maybe_regular_expander_graph` instead.",
  281. category=DeprecationWarning,
  282. stacklevel=2,
  283. )
  284. return maybe_regular_expander_graph(
  285. n, d, create_using=create_using, max_tries=max_tries, seed=seed
  286. )
  287. @nx.utils.not_implemented_for("directed")
  288. @nx.utils.not_implemented_for("multigraph")
  289. @nx._dispatchable(preserve_edge_attrs={"G": {"weight": 1}})
  290. def is_regular_expander(G, *, epsilon=0):
  291. r"""Determines whether the graph G is a regular expander. [1]_
  292. An expander graph is a sparse graph with strong connectivity properties.
  293. More precisely, this helper checks whether the graph is a
  294. regular $(n, d, \lambda)$-expander with $\lambda$ close to
  295. the Alon-Boppana bound and given by
  296. $\lambda = 2 \sqrt{d - 1} + \epsilon$. [2]_
  297. In the case where $\epsilon = 0$ then if the graph successfully passes the test
  298. it is a Ramanujan graph. [3]_
  299. A Ramanujan graph has spectral gap almost as large as possible, which makes them
  300. excellent expanders.
  301. Parameters
  302. ----------
  303. G : NetworkX graph
  304. epsilon : int, float, default=0
  305. Returns
  306. -------
  307. bool
  308. Whether the given graph is a regular $(n, d, \lambda)$-expander
  309. where $\lambda = 2 \sqrt{d - 1} + \epsilon$.
  310. Examples
  311. --------
  312. >>> G = nx.random_regular_expander_graph(20, 4)
  313. >>> nx.is_regular_expander(G)
  314. True
  315. See Also
  316. --------
  317. maybe_regular_expander_graph
  318. random_regular_expander_graph
  319. References
  320. ----------
  321. .. [1] Expander graph, https://en.wikipedia.org/wiki/Expander_graph
  322. .. [2] Alon-Boppana bound, https://en.wikipedia.org/wiki/Alon%E2%80%93Boppana_bound
  323. .. [3] Ramanujan graphs, https://en.wikipedia.org/wiki/Ramanujan_graph
  324. """
  325. import numpy as np
  326. import scipy as sp
  327. if epsilon < 0:
  328. raise nx.NetworkXError("epsilon must be non negative")
  329. if not nx.is_regular(G):
  330. return False
  331. _, d = nx.utils.arbitrary_element(G.degree)
  332. A = nx.adjacency_matrix(G, dtype=float)
  333. lams = sp.sparse.linalg.eigsh(A, which="LM", k=2, return_eigenvectors=False)
  334. # lambda2 is the second biggest eigenvalue
  335. lambda2 = min(lams)
  336. # Use bool() to convert numpy scalar to Python Boolean
  337. return bool(abs(lambda2) < 2 * np.sqrt(d - 1) + epsilon)
  338. @nx.utils.decorators.np_random_state("seed")
  339. @nx._dispatchable(graphs=None, returns_graph=True)
  340. def random_regular_expander_graph(
  341. n, d, *, epsilon=0, create_using=None, max_tries=100, seed=None
  342. ):
  343. r"""Returns a random regular expander graph on $n$ nodes with degree $d$.
  344. An expander graph is a sparse graph with strong connectivity properties. [1]_
  345. More precisely the returned graph is a $(n, d, \lambda)$-expander with
  346. $\lambda = 2 \sqrt{d - 1} + \epsilon$, close to the Alon-Boppana bound. [2]_
  347. In the case where $\epsilon = 0$ it returns a Ramanujan graph.
  348. A Ramanujan graph has spectral gap almost as large as possible,
  349. which makes them excellent expanders. [3]_
  350. Parameters
  351. ----------
  352. n : int
  353. The number of nodes.
  354. d : int
  355. The degree of each node.
  356. epsilon : int, float, default=0
  357. max_tries : int, (default: 100)
  358. The number of allowed loops,
  359. also used in the `maybe_regular_expander_graph` utility
  360. seed : (default: None)
  361. Seed used to set random number generation state. See :ref`Randomness<randomness>`.
  362. Raises
  363. ------
  364. NetworkXError
  365. If max_tries is reached
  366. Examples
  367. --------
  368. >>> G = nx.random_regular_expander_graph(20, 4)
  369. >>> nx.is_regular_expander(G)
  370. True
  371. Notes
  372. -----
  373. This loops over `maybe_regular_expander_graph` and can be slow when
  374. $n$ is too big or $\epsilon$ too small.
  375. See Also
  376. --------
  377. maybe_regular_expander_graph
  378. is_regular_expander
  379. References
  380. ----------
  381. .. [1] Expander graph, https://en.wikipedia.org/wiki/Expander_graph
  382. .. [2] Alon-Boppana bound, https://en.wikipedia.org/wiki/Alon%E2%80%93Boppana_bound
  383. .. [3] Ramanujan graphs, https://en.wikipedia.org/wiki/Ramanujan_graph
  384. """
  385. G = maybe_regular_expander_graph(
  386. n, d, create_using=create_using, max_tries=max_tries, seed=seed
  387. )
  388. iterations = max_tries
  389. while not is_regular_expander(G, epsilon=epsilon):
  390. iterations -= 1
  391. G = maybe_regular_expander_graph(
  392. n=n, d=d, create_using=create_using, max_tries=max_tries, seed=seed
  393. )
  394. if iterations == 0:
  395. raise nx.NetworkXError(
  396. "Too many iterations in random_regular_expander_graph"
  397. )
  398. return G